美文网首页
如何构建一个健壮性的服务

如何构建一个健壮性的服务

作者: lfboo | 来源:发表于2021-11-13 21:02 被阅读0次

在大数据的时代、流量爆发的时代,怎么保证我们的服务安全、稳定是每个企业、每个开发者需要关注的问题。于是微服务、分布式、大数据等等一些高大尚的名词出现了...。然而无论多么复杂的系统或者框架,它们肯定都是由某些细小的东西组成的。

分布式

分布式的出现就是为了解决单体服务无法承受很高的压力,同时解决因为单体服务故障而导致整个服务不可用的情况。分布式服务肯定是由多个节点共同组成的,每个节点都可以分担一些压力,这样本来1台机器可以支撑1w qps的请求,通过10台机器,就可以达到10w qps的请求。而且通过把节点部署在不同机房,减少因为节点集中而造成集体瘫痪的现象。如果我们有只有一台机器,那么不管部署在哪,一挂整个服务就不可用,如果我们有10台机器,分别分布在城市A、城市B....,这样就算因为天气或者其他原因导致城市A的节点不可用,这时候我们只需要将本来接入城市A的流量给掐掉,把流量分摊给其余9个节点,整个服务还是可用的,实现了高可用了。既然有多个节点,那么我们如何将流量均摊或者按照我们的策略分给单个节点?

负载均衡

对于分布式的服务,一般常见的负载均衡算法有:

轮询法

将请求按顺序轮流地分配到各个节点上,比如第一个请求分给节点A,第二个请求分给节点B...,轮询法对于每个节点都非常公平,不会关心当前节点的状态(比如节点的连接数、负载等等)。所以一般使用轮询法的时候,每个节点的配置尽量保持一致(同样的CPU核数、同样的内存...)。如果存在某个节点的配置过低,那么对于同样的流量它的压力可能更大,这样在轮询的情况下,反而不是很公平,分配到低配置节点的流量也许会因为负载太大而承受不了。

随机法

相比轮询法的轮流分配,随机法可以通过一些随机的算法来选择其中一个节点来分配,比如当前有10个节点,你可以通过rand(1,10)来随机选择一个节点。由概率统计学可以得知,随机算法会随着调用次数的增多,而趋于平均,所以通过随机法的分配方式最终的效果和轮询法差不多。

IP哈希法

有时候我们希望某个客户端的请求固定打到某个节点上,比如客户端A的请求,每次我都希望它被分配到节点A上。这时通过轮询或者随机肯定是不行的,通过客户端的IP来分配到是不错,可以根据客户端的IP来hash得到一个数值,然后通过这个数值对节点的数量取模,取模后得到的数字结果就是这个请求要分配的节点序号。比如客户端的IP是192.168.10.1,然后通过hash函数得到的结果是:hashIndex(192.168.10.1)%10=5,那么5就表示当前应该分给序号是5的节点。

加权轮询法

现实情况中,如果出现某些个节点的配置不一样,通过权重来分配流量比较合理,比如节点A是2核CPU的,而其余节点都是4核CPU的,很明显节点A能承担的负载是相对较低的,这时候可以根据权重来分配流量,配置低的节点权重就低一些,然后根据权重顺序的将流量分配到各个节点上。比如正常轮询的时候,每个节点轮流分配,加权之后,除了节点A之外都是轮流分配,节点A可能是其余节点每分配两轮之后,再分配一次。

加权随机法

与加权轮询法一样,加权随机法也根据每个节点配置的权重来分配流量的。不同的是,它是按照权重随机请求后端服务器,而非顺序。比如可以根据核数来分配概率,假设节点A是2核CPU的,而其余9个节点都是4核CPU的,那么节点A每次被分配到的概率就是2/(4*9+2)。

最小连接数法

最小连接数法应该是比较灵活的负载均衡算法了,当每个节点的配置不相同的时候,除了要人为配置权重的算法之外,还可以根据节点的连接数来分配流量,这就是最小连接数法。每次根据节点当前的连接情况,动态地选取其中连接数最少的一个节点来分配流量,尽可能地提高每个节点的利用效率,更加合理地分配流量。

服务注册与发现

ip + port

当我们有了多个集群,也设计好了负载均衡算法,接下来要解决的就是一个请求如何找到对应的节点,比如A请求应该是分给A节点的,那么A请求如何找到A节点?最简单的方式就是ip+端口的方式:把ip和端口告诉调用方,调用方根据lb选择对应的ip节点,这样就找到了服务,这种方式比较常见的就是配置nginx的upstream,把nginx作为我们server的代理层,由它去帮我们发现服务。但是这种方式的缺点就是需要人工干预,当我们需要动态添加一个或者删除一个节点的时候,都需要修改nginx的配置,如果线上某个节点出现故障,人工去改配置是不是显得有些缓慢。

如何构建一个健壮性的服务

注册和发现

如何构建一个健壮性的服务

也许你会觉得ip和port的方式还行,毕竟在节点不多的情况下,加加减减配置也是很快的。然而在大型微服务系统中,我们会把服务拆的更细,我们会根据业务分类把整个应用拆成一个一个微服务,每个微服务做高可用,所以每个微服务也是由多个节点组成的。在这么多节点的情况下,通过ip+port的方式来管理,那得多耗费人力和不安全。于是服务注册和发现就出现了,通过服务注册和发现可以自动管理节点,不需要人为干预,极大的提高了运维的效率。

服务注册:把自己的ip和端口告诉一个管理者,同时可以设置一个标识name。

#伪代码 注册一个鉴权节点
discover->register('192.168.0.1',8000,'auth')

服务发现:通过注册的name去找到对应的节点。

#伪代码 发现一个节点
discover->find('auth')

服务注册与发现通过心跳来检查节点存活状态,如果节点心跳失败,那么就会自动摘除,当然我们的discover也得是集群,高可用。目前一般使用consul、etcd、zookeeper等来实现。

缓存

随着信息的发展,数据量越来越大,用户越来越多,单单靠数据库提供读写已经不能满足响应快的需求,因为IO是相对耗时的,因此我们一般会在db的前面拦上一道缓存,优先读取缓存,缓存失效后我们再去数据库中读取,读取到数据再回写到缓存中,这样形成一个闭环。通过缓存,我们可以保证绝大部分的请求走缓存,不仅达到响应速度的提升,还会起到保护db的一个作用。既然用了缓存,那就避免不了数据性一致的问题。当我们尝试更新一条数据,我们是先更新缓存,还是更新数据库?

先更新缓存,再更新数据库

  • 假设先更新缓存成功,然后更新数据库失败,那么就会导致数据不一致,而且在缓存失效后去数据库读取的数据也是老的。
  • 假设现在有两个请求A、B同时进行操作A更新缓存B更新缓存B更新数据库A更新数据库由于网路问题A延迟了,那么可以发现B更新数据库的数据被A覆盖了。

先更新数据库,再更新缓存

  • 假设先更新数据库成功,然后更新缓存失败,那么也会导致数据不一致,但是相比先更新缓存,再更新数据库的模式,在缓存失效后,可以读到正确的数据。
  • 假设现在有两个请求A、B同时进行操作A更新数据库B更新数据库B更新缓存A更新缓存由于网路问题A延迟了,那么可以发现B更新的缓存数据被A覆盖了。

撇开双写不一致的问题不说,更新缓存这个动作不推荐使用,因为有时候缓存里面的数据是个比较复杂的数据,它是个综合体,需要读几张表的数据做个聚合,并且如果你用的json string,那么每次还要反序列化,更新后再序列化,整个开销还是非常大的。而且如果在某一时刻,你更新了10次,但是一次读也没有,那么白白浪费了多次更新缓存这个操作。

先删除缓存,再更新数据

当A请求来删除缓存,B请求读缓存:

  1. A删除缓存
  2. B请求缓存,发现缓存不存在
  3. B请求数据库,把读到的旧值回写到缓存
  4. A将新值更新到数据库

解决这个问题方式一般用延迟删除,在A更新完数据库后,再执行一次删除。

先更新数据库,再删除缓存

同理如果更新数据库成功了,但是删除缓存失败了,那么也会导致数据不一致,或者你的缓存是主从架构,主删除成功了,但是还没来得及同步到从,这时候读取的是从服务器,那么也会造成短暂的数据不一致问题。

其实无论哪种方法都不能保证数据库缓存双写一致的问题,更何况在分布式系统中。只有选择一种风险更小的,或者更适合自己业务场景的方式。剩下就是接监控和报警,在一些对数据一致性要求比较高的场景中,通过监控和报警来及时通知异常数据,然后对异常数据进行修补操作是个不错的选择。

接口限流

为什么接口需要限流,首先每个接口干的事不一样,有的接口也许只返回一些静态的配置,有些接口需要进行复杂的运算,还有些接口需要大量的IO...。不限流的话,对于一些比较耗时的接口来说,突发的流量可能把我们的机器打挂或者背后的DB打挂。 限流怎么估算?一般通过压测来预估自己接口能承受的qps,这样超过预估的qps,我们拒绝处理。好的限流算法也是非常重要的,常见的限流算法如下:

计数限流

来一个就累加一个,在单位时间内当总数到达设定的界限后,就拒绝服务。 这种方案的缺点就是突发流量支持不太好,比如1s限制100个,前100ms就把用完了,剩下的900ms内都是无法服务的。

count++
count--

固定窗口限流

相比计数限流,固定窗口多了个窗口的概念。固定窗口限流的主要思想就是将某个时间段当成一个窗口,在这个窗口内有一个计数器,这个计数器用来统计在这个时间窗口内请求的次数,当请求的次数超过阈值,那么就会限流。当开启下一个窗口的时候,会进行重新计数。但是固定窗口有个缺点:假设我的限流的是100qps,我的窗口是以s为单位的,第一个窗口的后0.5s把100qps消耗完了,然后第二个窗口的前0.5s把100qps也消耗完了,那么两个加起来1s就200qps,显然不符合我们的限流规则。

如何构建一个健壮性的服务

滑动窗口限流

为了解决固定窗口带来的问题,我们可以将窗口设置成不固定的,每次请求到来的时候,往前推1s,那么在这1s的窗口内,如果没达到限流上限那么就提供服务,否则拒绝服务。但是滑动窗口也有个问题,就是没法解决流量突发的问题,比如前1ms,把限流用完了,剩下的999ms都是无法服务的。

如何构建一个健壮性的服务

漏桶限流

漏桶算法的思想是:外部速率是变的(请求有密集有稀疏),而桶的容量是固定的,并且总是以恒定的速率出桶,当处理不过来了(桶满了),多余的请求就会被限流(桶溢出)。漏桶法适合以恒定速率处理的场景。

如何构建一个健壮性的服务

令牌桶

令牌桶的思想是以恒定的速率像向里放入令牌,当桶里的令牌满了,那么就不会放入,说明此时的请求速度跟不上令牌放入速度。这样在服务闲暇时,桶里会有足够的令牌,在突发流量的时候,因为桶里已经有足够的令牌了,也可以一定程度上满足突发流量。令牌桶算法支持出桶的速率是任意的,每次都需要从桶里获取令牌,如果获得了令牌表示可以处理任务,没拿到令牌就是限流了。

如何构建一个健壮性的服务

放入令牌的动作也不需要单独起个线程去处理,可以在每次获取令牌的时候利用时差和速率顺带算出应该放入多少令牌进去。

速率:speed = 1/n #例如 1s 10个, 那么速率就是每1/10秒放入1个
时差:difftime:=now-lasttime
放入令牌:difftime/speed #这里注意算出的token别超出桶的容量

服务熔断

为什么需要熔断机制?在微服务中,由于服务拆分的很细,经常会出现服务A->服务B->服务C,如果C服务因为某些耗时操作,导致慢查询,接口的RT线性增长,这时会导致B服务的所有请求都超时,随着超时越来越多,tcp连接被打满,服务B也出现问题,服务B出现问题之后,会导致A的超时也逐渐增加,同样也出现A服务不可用的情况。可以发现因为服务C的问题导致相关联的服务都出现不可用的情况,引起雪崩。我们应该在服务C出现连续不可用的时候,切断服务B与C的通信,这就是熔断。当然我们也不能一直熔断,如果服务C好了,我们还还得继续提供服务,那么这就涉及到熔断算法了。

熔断策略

  1. 统计一段时间内的请求总数
  2. 统计这段时间内请求的失败率
  3. 如果1和2都满足,触发熔断
  4. 熔断一段时间后半开启,尝试放入一点量
  5. 半开启后如果还是出现很多失败,再次熔断,否则熔断器关闭。

当请求总数达到我们设置的数量且失败率也达到我们设置的标准后,就触发熔断。这时候熔断器会告诉我们服务不可用了,这样下次过来的请求,会直接返回。当过了一段时间之后(时间可配),假设依赖的服务已经恢复了,这时候熔断器会尝试放入一部分流量进去试探,如果此时还有很多错误,说明依赖的服务还是没恢复,那么会再次触发熔断,并且等待下次半开启的时间。如果半开启的流量都ok,那么就会关闭熔断,服务恢复。

如何构建一个健壮性的服务

服务降级

在服务治理中,降级也是比较重要的一个手段。当业务流量出现峰值的时候,导致某些不重要的服务出现故障,而这些故障服务可能造成连锁反应,尽而影响主体业务,这时候一般可以停掉一些不重要的服务(比如运营位、评论...)。

手段

  • 整个服务停掉
  • 相关的接口直接返回空
  • 接口依赖DB资源切掉,转成从本地内存读取兜底数据

当然降级一般可以熔断配合一起使用。

if break { //熔断
   return localCache //降级走本地缓存
}

守护进程 + 平滑重启

如果我们服务在运行期间,突然出现异常退出,这时候假设没有一个自动恢复的功能,那岂不是很尴尬。

守护方式

  1. 如果服务上k8s,那么k8s会在pod退出时,自动拉起。
  2. 通过第三方的supervisor,管理我们的进程
  3. 通过系统systemd来管理我们的进程

平滑原理

需要注意的是在服务重启的过程中,要注意平滑。不能因为重启造成正在处理的请求出现504的情况,一般会通过主进程fork子进程的方式,主进程负责处理老的请求,主进程处理完成之后,自动退出。新的请求全部交给子进程处理,主进程退出后,子进程也就是新的主进程了。

分布式链路追踪

在分布式系统中,由于服务的拆分,一个网关接口背后可能是许许多多的微服务接口层层嵌套而成的,这样无疑增加了问题的排查难度。比如A->B->C->D, A因为错误找B,B说是C的错误导致的,C说是D错误导致的。如果有一种方式能将分布在各个地方的节点串起来,最终通过界面展示出各个节点的耗时、ip、错误等信息,那就可以极大的提升排查问题的效率。

分布式链路追踪(trace)的出现解决了这个问题。
链路追踪一般包括3个核心的步骤:数据采集、数据存储和数据展示。由于数据采集需要侵入代码,那么必然会造成各种不同的写法,于是OpenTracing出现了,OpenTracing是一套分布式追踪协议,和语言无关,具有统一的接口规范,方便接入不同的分布式追踪系统。OpenTracing语义规范中定义的数据模型有Trace、Span以及Reference。

Span

Span表示一个独立的工作单元,它可以是一次rpc调用,一次函数调用,甚至是你认为属于span模块的调用。 通过span圈定我们关注的模块,span一般包含:

  • span的name,比如依赖方的服务名称
  • 开始时间戳
  • 结束时间戳

Trace

Trace表示一条完整的追踪链路,例如:一个请求的整个生命过程。一个 Trace是由一个或者多个Span组成的有向无环图(DAG)。

        [Span A]  ←←←(the root span)
            |
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                       ↑
                                       ↑
                                       ↑
                         (Span G `FollowsFrom` Span F)

按照时间轴的方式展示如下:

––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]

Reference

一个Span可以与一个或者多个Span存在因果关系,这种关系称为Reference。OpenTracing目前定义了两种关系:ChildOf(父子)关系和FollowsFrom(跟随)关系。

  • ChildOf:父Span的执行依赖子Span的执行结果,比如对于一次RPC调用,父Span同步等待子Span的返回结果。这时候子Span就是ChildOf父Span。
  • FollowsFrom:父Span的执行不依赖子Span的执行结果,比如一些异步流程。

每个请求的链路都有一个唯一的traceID,通过traceID和spanID,可以把一条链路上的所有span关联起来,整个请求链路一目了然。常见的分布式链路追踪有 Uber的Jaeger、Twitter的Zipin、阿里的鹰眼...

如何构建一个健壮性的服务

日志

通过日志帮我们排查问题也是个常用手段,根据不同的场景,我们将日志级别分为以下几种:

  • DEBUG:调试等级的日志,调试程序时经常使用的类型。
  • TRACE:跟踪等级的日志,指一些包含程序运行详细过程的信息。
  • INFO: 信息等级的日志,打印一些自己认为需要的info信息。
  • WARN: 警告等级的日志,可能是潜在的错误,需要进一步判断问题的严重性,一般不影响程序正常运行。
  • ERROR:错误等级的日志,指某个地方发生了错误,影响正常的功能,需要确认修复的
  • FATAL:致命等级的日志,程序发生崩溃,异常退出等严重性错误。

监控

如何构建一个健壮性的服务

服务的健壮性离不开监控,通过监控我们可以看到服务的qps情况、http错误情况、耗时情况、cpu负载情况、以及我们自己想要监控的业务数据情况...。监控系统的三要素:收集指标、存储数据、页面展示。 这其中被广泛使用的的莫过于Prometheus+Grafana。普罗米修斯监控上报的指标有4种metrics类型:

Counter:

计数器是比较简单的指标类型,计数器的值只能增加或者重置为0,比如统计接口的请求量、http错误的次数等都可以用counter。

httpReqs := prometheus.NewCounterVec(
  prometheus.CounterOpts{
   Name: "http_requests_total",
   Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
  },
  []string{"code", "method"},
 )
 prometheus.MustRegister(httpReqs)
 httpReqs.WithLabelValues("404", "POST").Add(42)

m := httpReqs.WithLabelValues("200", "GET")
m.Inc()

Gauges

Gauges可以用于处理随时间增加会动态变化的指标,比如像内存使用率、CPU负载等等。

opsQueued := prometheus.NewGauge(prometheus.GaugeOpts{
  Namespace: "our_company",
  Subsystem: "blob_storage",
  Name:      "ops_queued",
  Help:      "Number of blob storage operations waiting to be processed.",
 })
 prometheus.MustRegister(opsQueued)

 // 10 operations queued by the goroutine managing incoming requests.
 opsQueued.Add(10)
 // A worker goroutine has picked up a waiting operation.
 opsQueued.Dec()

Histogram

直方图,一个histogram会生成三个指标,分别是_count(数量)、_sum(累计和)、_bucket(桶)。

  • _bucket:对每个采样点进行统计打到各个桶(bucket)中
  • _sum:对每个采样点值累计和(sum)
  • _count:对采样点的次数累计和(count)

比如现在有100份蛋糕,蛋糕的质量都是小100g的,我们希望把这100份蛋糕按质量分到对应的盒子里,现在有5个盒子分别可以盛放0-20g20-40g40-60g60-80g80-100g的蛋糕。那么_bucket就是这5个盒子,假设20g的盒子里面装了5g、10g、15g 3个蛋糕,那么_count就是3,_sum就是5+10+15=30g。

CakeHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
  Name: "cakeHistogram",
  Help: "cake histogram",
  Buckets: prometheus.LinearBuckets(20,40,60,80,100),
 })
cases:=[]int{1,2,3,4,...}
for i:=0;i<len(cases);i++{
  CakeHistogram.Observe(cases[i])
 }

Summary

summary和histogram类似也会产生三个指标,分别是_count、_sum和quantile,_count和_sum与histogram的概念相同,quantile的含义是分位数,summary可以在定义时指定分位数,如5分位、9分位、99分位、999分位,999分位的概念就是比这个数小的数占99.9%。如果你有N个样本值,首先要从小到大排序,然后取出排在φ *N的值。summary可以通过百分比的概率来反应整个指标的情况。一般会对φ加一个波动范围,比如0.9:0.1,那么最终φ在0.89-0.91之间。

GradeSummary = prometheus.NewSummary(prometheus.SummaryOpts{
  Name: "man_grade",
  Help: "man grade",
  Objectives: map[float64]float64{0.5:0.01,0.9:0.001,0.99:0.01,0.999:0.01},
 })
var salary = [10]float64{90,87,88,84,99,100,91}
for i:=0;i<len(salary);i++{
 GradeSummary.Observe(salary[i])
}

指标上报的前提首先要在你的机器上安装Exporter组件,然后prometheus server会主动过来拉采集器的数据,或者采集器主动把数据推到push gateway中,prometheus server再对从push gateway进行数据的拉取。指标一般都会临时存在机器的内存里,这也是为什么一般发版的时候,会出现曲线抖动的情况。

告警

告警是我们第一时间了解问题的手段,一次业务的错误可以告警,一段时间内qps的抖动可以告警...,常见的告警的手段有:

  1. 企业微信
  2. 邮件
  3. 短信
  4. 电话

CI/CD

在没有CI/CD的时候,上线代码的任务是非常繁琐的,需要多人合作。

如何构建一个健壮性的服务

你需要自己去编译,然后跑单元测试,如果中途发现错误,还得修改代码,修改代码后,又得手动去编译、测试...,这一系列重复的动作,无不降低从开发到部署的效率。

CI

如何构建一个健壮性的服务

持续集成(Continuous Integration):,开发人员能够频繁地将其代码集成到公共代码仓库的分支中。CI可以在源代码变更后自动检测、拉取、构建和进行单元测试。CI的目标是快速确保开发人员新提交的变更是好的,并且适合在代码库中进一步使用。

CD

如何构建一个健壮性的服务

持续交付(Continuous Delivery):持续交付在持续集成的基础上,将集成后的代码部署到预发环境中。

如何构建一个健壮性的服务

持续部署(Continuous Deployment):持续部署则是在持续交付的基础上,把部署到生产环境的过程自动化。

通过CI/CD我们可以解放重复性劳动、更快地修复问题、更早的交付成果,实现了开发运维一体化,大大提升了交付效率。

配置中心

为了减少我们项目中的硬编码,针对一些可能经常变更的配置信息,一套成熟的配置中心能给我们的项目带来不少的好处:不用因为一个小配置,而重新修改代码(比如一个商品的价格,今天是9.9元,明天做活动,希望改成6.9元)。根据场景,把静态的常量改成读取动态的配置,可以提升交付的效率。

实现

一般是在项目发布的时候,去远程的配置中心把对应的配置文件拉取到本地,程序根据配置文件把对应的配置映射到自己的变量中,相应的代码读取对应的变量。

特点

  • 可视化配置后台,方便操作
  • 版本记录,会记录每一次配置修改的记录,出了问题,方便回溯
  • 回滚,如果新改的配置有问题,可以回退到之前的配置。

故障演练

故障演练类似于军事演习,虽然线上稳定,但是如果出现问题,如何排查和解决?目的是锻炼程序员的问题排查能力和快速定位问题的能力。那么如何搭建一套演练环境?直接用测试环境肯定不行,首先不能影响正常测试,其次测试环境和真实环境还是有些差别的。预发和生产更不行,它们的内部连接的都是真实的线上资源,不能影响线上。

环境搭建

于是需要搭建这样一套接近真实环境的演练环境,这个演练环境的资源(DB、缓存...)需要和生产隔离,并且把线上的DB等资源copy新的DB中。接下来是流量,流量要接近真实流量,最好是真实流量,可以使用类似GoReplay这样的http流量回放神器,在lb层把流量进行复制,然后把复制的流量在演练环境进行回放达到真实流量回放的效果。

演练什么

  • DB切断
  • 缓存切断
  • 某个服务切断
  • ....

模拟一些资源或者其他的故障,当故障出现时,第一时间应该是报警,然后去查看监控和日志来定位是哪个服务的哪个环节出现了。

压测

通过压测来摸清线上的处理极限来达到提前预防的效果,压测环境也要接近真实环境,压测环境理论可以使用演练环境,如果条件充足,也可以单独搭建一个压测环境。

压测对象

  • 可以是一个接口
  • 可以是一块业务
  • ...

压测评估

  • RT:观察响应时间
  • ERROR:观察错误信息
  • CPU:观察cpu负载
  • MEM:观察内存负载
  • DB:观察数据库负载
  • 连接数:观察网络连接数
  • IO:观察磁盘IO
  • QPS:观察qps曲线

最终通过压测评估出支撑的qps,极限的瓶颈在哪,未来如果流量增长,优化点在哪,如果是cpu密集型的,应该加机器资源,如果是IO密集型的,可能要加DB相关的资源了。

编码健壮

以上介绍的都是服务级别的健壮,下面我们来介绍下程序层面的健壮。如何优雅的编写一套代码,对于一个程序员来说也是非常重要的。

推荐用 return 代替 else

// bad case
if a {
  do_a()
} else if b {
  do_b()
} 
// good case
if a {
  do_a()
  return
}
if b {
 do_b()
 return
}

重试

当我们在进行rpc调用的时候,对于一些非常重要的业务场景,可以给予几次重试的机会。

for i:=0;i<2;i++{
  if _,err := rpc.getUserinfo();err==nil{
     break
  }
}

连接池

通过连接池来复用连接,避免通过三次握手来频繁的建立新的连接。

redisPool := newRedisPoo()
conn := redisPool.Get()

超时控制

任何一个rpc请求,都应该给予一定的超时时间,避免造成雪崩现象。

conn, err := net.DialTimeout(netw, addr, time.Second*2) 

避免资源重复调用

在一个业务中,经常会出现func A()->func B() -> func C(),假设每个func都需要获取用户的信息,千万不要每个func都去获取一遍(除非对实时性要求非常高的场景),通过参数传递的方式来减少不必要的查询。

func A() {
 user := DB.getUser()
 B(user)
}
func B(user *User) {
  C(user)
}
func C(user *User) {
}

函数体代码不要过多

当一个函数的代码行数非常多的时候,会不利于阅读。

func doSomething() {
 //此书省略1000行
}

函数命名要符合实际意义

例如获取最近登陆的用户。

//bad case
func getLoginUser()
//good case
func getRecentLoginUser()

捕捉异常

不要因为一些异常而导致你的程序退出。

defer func (
  if err:=recover;err!=nil{
    log.Error("something panic (%+v)",err)
  }
){}

相关文章

网友评论

      本文标题:如何构建一个健壮性的服务

      本文链接:https://www.haomeiwen.com/subject/cuwxtrtx.html