打造三星高照的系统- 软件的稳定健康维护的模式与反模式的心得

福如东海成流水,寿比南山不老松。横批 福禄绵延。 这是我们对人的期望。希望他reliable(寿),scalble(福),maintainable (禄),这就是通常人们所说的三星高照。 人是如此,应用亦是。 我们也希望我们的系统三星高照。我们在实际工作中对系统的稳定的理解增加了一些经验。我们这里把这些经验break down,作为一个总结。

 

数据无关的,尤其是企业级应用。那么这福寿禄三星各管那一方面呢。

  1. Reliability。简而言之就是活得长。在各种错误下, 仍然屹立不倒。比如软件错误,硬件错误,人为错误。

  2. Scalability。 就是身体健康,啥好事都能赶上。

  3. Maintainability。 就是活的顺心, 各种维护不仅轻松,而且省钱。 

寿比南山

活的越久,经历的事情越多。一个系统,也会遇到各种问题。要想活命,得躲避致命疾病。它一般会遇到什么问题呢。

  • 软件问题

这个问题是我们的重点内容,下面会详细讲

  • 硬件问题

硬件问题实际上包括hypervisor, vm/pod, network, disk 等等除了问题。 这些通常有专门的基础设施的团队负责,通常是在maintianability 的范围内。

  • 人员问题

操作错误,比如修改参数错误或者调用API没有调用对。或者恶意操作

代码错误,写了有bug 的代码。

设计错误   设计的场景不符合实际需求。

人员的问题的解决通常需要组织采取一些的行动。

  • 建立严格遵守的流程 (测试, 发布 )

  • Review (design, code)

  • Training

  • Testing 

  • Monitoring and alerting

导致软件不稳定的常见模式

Integration Point

集成点,也是瓶颈点。通常在系统中常见的模式facade模式。在海量数据的情况下,集成点的不稳定会造成所有上游系统的不稳定。 他的崩溃会导致所有涉及到它的服务的不稳定。从系统设计上来来讲 

  1. 我们必须要考虑HA。

  2. 必须要考虑是否适合一个integration point 来面对所有的流量。

 

尤其这个集成点是非关键服务,比如tracking,log 那么调用的时候,如果出现错误,是否要ignore, 是否要进行保护。 

如果是关键服务,比如sigin , 不能有任何ignore, 那么就需要对这些服务要特别关注。如果有问题,必须得到第一优先级的抢修。

 

集成点有两种模式

  1. 作为服务存在,所有的相关服务都要通过它,典型的是sigin capacha。 当出现问题的时候,那么这种服务是可以通过traffic route 来进行colo 级别的切换, 更高级一点service discovery 来进行服务切换,或者colo 级别的切换。 但是当他完全不能工作时候,系统 基本就不能工作了。但是这种抢修相对比较容易。 

  1. 作为proxy 存在, 比如gateway, 那么这种服务如果坏掉,所有的流量都要经过它,他如果坏掉,那么就没有办法。这种抢修就比较困难, 你很难切断流量去修复。

 

Chain Reaction

链式反应, 通常的案例就是一个pool 有n台机器,当流量过大的时候,或者流量不均衡,导致一台机器承受不了,这个机器倒下, 流量转到其他机器, 其他机器流量过大,继续倒下。最后导致整个pool 所有机器都不能正常工作。 这种问题传递是横向的。 

通常不论在kubernates 还是 openstack上都有monitoring 系统和Capacity 团队来进行扩容。 

在迁移过程中,这种情况是最容易发生的,当迁移新pool 如果新pool 的容量不够,或者容量不均衡,很容易发生这种事情。 另外 流量迁移通常不是一次性迁移完, 慢慢迁移,那么新pool

的机器通常也不是一次到位,那么要根据下一次的流量的迁移数量来增加,那么就需要有个前提

流量迁移要控制的准确,导入的流量要在每个colo 每台机器上是均衡的。否则也会发生链式反应。这是我们做流量迁移的重要观察点。

Cascade Failure

雪崩效应。 这种错误是纵向的,当一个中间环节出错后,他的前面的系统如果处理不好,也会导致所有环节的崩溃。 比如, 当一个环节service 挂住,他的前置如果没有设置合适的timeout, 就会导致线程积压,从而是前置的前置线程积压,最后所有链条上的系统全部崩溃。

这种问题通常会有很多方式解决, 比如circuit breaker , timeout, handshake, failfast 等等。

现在的成熟系统,雪崩效应该发生的几率不高,最常见发生的就是timeout 没有设置, 缺省的timeout是0 就是无限等待,在线程积压的情况下, 通常线上都会重启或者自动重启,这可以缓解一部分情况,所以下游一两个服务出现异常, 情况还算是可控。 但是如果DNSResolver或者GTM 出现问题,这种情况才是致命的。 

Users

用户使用错误也会导致系统的不稳定,通常这种情况在系统迁移的时候特别明显。

  • 系统变化

老的系统支持用户的不规范使用, 新的系统不支持了。短时间内会对系统造成巨大的冲击。导致系统的服务不正常,导致系统不可用。这种情况通常很难定位,举个例子,很多系统外部的用户都用脚本,比如perl , python来调用soa 的服务, 利用脚本 通常会自己设置request header, 很多用户的脚本在原来的script里面没有遵循规范,content-type 就没有设置。 在perl 里, 如果你不设置,default就是content-type: application/x-www-form-urlencoded. 这个content-type 在不同的web server 有不同的处理, 在g1,当读request parameters 的时候, 这个流不会被读出,只是复制。 在tomcat ,在前置端一但有逻辑读request parameters,这个流就被读出。当逻辑部分再读request 流的时候就出现null pointer。 

用户的不规范应用一定要避免。 我们应该有些方式来解决,通常的策略是对老的不规范的用户向后兼容, 对新的用户就不支持这种用法。所以在程序里需要有向后兼容性的逻辑。

  • 用户操作失误

不同的用户的操作权限一定要有管理,不能有无限权限,同时 必须有audit。拥有最高管理员权限的操作,一定要有监控和rollback 方案。通常会有各种工具来实现标准化操作。

Security Vulnerability

系统的安全漏洞。当系统出现安全漏洞的时候,那么就算系统服务正常,也是不能够使用的。安全漏洞是一个专门的课题, 在应用系统中, 用户最常见的安全漏洞通常发生在使用第三方的library中,使用第三方的library 要非常小心,一定要使用系统 authentication 的library。我们一般不推荐所有的第三方library 都要从系统central 上获得。

Block Threads

在并发模型变成中,线程死锁是一个明显导致系统崩溃的例子。 在有锁的应用中, 最明显的标记是当流量增大的时候,cpu 的usage 会急剧上升。这个时候内存就不是capacity 的考量依据,是线程的可用数量。 

那么在编程模型中,有另种类型, 一种是单机内存锁, 一种是分布式锁。 

单机锁, 通常和synchronize, CAS, AQS 相关。这个可以通过thread dump可以看出来。

分布式锁, 有数据库的实现如BES、有一些系统的实现,如zookeeper。这个很难通过thread dump 看出来。 只能看到很多wait 线程。

当系统有锁的时候, 通常系统的吞吐量会有一个瓶颈。

Block thread 有四个条件

  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。

  4. 循环等待条件: 互相持有锁

Attacks of self-Denial

当一个正常的业务应用, 部署了一个秒杀模块的时候。 这个时候就是自取灭亡的时候了。在一个短时间内,一个秒杀模块吸收了巨量流量。 这个时候, 正常的业务应用希望能正常使用, 实际上是不能了。 这个时候,就需要把秒杀和正常业务分开。 秒杀也是 一个微服务。 事实上,微服务的出现从来不是一个代码层面的划分,而是真正的业务划分。

Characteristics of a Microservice Architecture


Scaling Effect

规模效应。当一个系统规模用户增长到一定程度的时候,量变引起质变。大家都听说过square-cube 规则。 能不能出现楼房一样大的蜘蛛呢,从square-cube 的规则,蜘蛛的腿结构撑不起楼房一样的蜘蛛的重量。 举个例子, 当12306开始做火车站售票网站的时候, 很多人不是很理解, 有些程序员说用hiberate 加上一个spring mvc 不是轻松搞定么。 这个是典型的对规模效应缺乏理解, 互联网应用一个难点就是用户是不能预测的,海量用户带来的数据存储, 交易速度,尤其售票网站还有座位互斥。 这个就是互联网应用的特点。分布式, 巨容量,高并发,分区,分布式锁等等。 

当系统不能满足爆发性用户的需求的时候,那系统基本上是不能用了。必须考虑重构。

 

Unbalanced capacity

系统的流量导入,必须要稳 (balance),准 (accurate), 狠 (rollback immediately)。 在日常服务中,流量均衡是非常重要的, 否则承担系统最大的那个vm 就是最短的那块板。流量均衡在系统 要考虑 几个方面

  1. GTM 管理的web tier 是否连接了所有的app tier

  2. GTM 的L7 rule 是否是照顾了所有的web tier

  3. UFES 的 DC gateway 是否照顾了所有的app tier

  4. 不同colo 的 AZ 是否enable 了web tier,有些时候GTM 配了web tier 但是没有enable。

 

从流量的metrics 上是可以看到各个colo的流量是否均衡的。

从系统的机器的配置上看, 他们的型号也要一样。 否则尽管流量相同, 配置最差的那台机器也是最短的那块板。从pool provision的策略上看, 型号也是也一样的。但是我们遇到过型号一样,但是年代不同导致老的型号性能特别差的问题。 

硬件的问题其实比较难以发现,因为大部分程序员对硬件并不像infrastructure 团队那么了解。但是我们能从系统metrics 上看出这些问题。

Slow Response

有去无回这个系统肯定是挂了。大部分的service call 是同步call ,当一个调用链特别长的时候,slow response 的 副作用就越来越明显。 从调用的依赖层面来看, 出错的机率也越来越高。一个service call 应该是调用链越少越好。当发生长调用链的时候,那就要考虑messaing 系统。

从原则上说,service call 越快越好。 但是这个标准是什么,还是要根据每个服务的需求来定。 从原则上来说, 一个service call 在50ms 以内, 最大不要超过2s。 如果超过2秒,没有特殊的原因,一定是不可以的。性能优化,一定要持续做的。

从系统性能方面要不断优化,从框架层面, service call 的框架也在不断优化,从开始xml based 到现在 json base的越来越轻量级,框架的处理也越来越快。系统 的发展 从xml -> Axis->SOAP->Json    对应的框架  trading ->SHOPPING->SOA-> Ginger1(JERSEY 1.0 )-> Ginger2 (JERSEY 2.0)

SLA Reversion

每个service 对外服务的相应时间的约定,这个是强制约,必须遵守。 如果响应时间超过SLA,规定的时间,可能系统能够工作,但是有可能收到罚款。 在系统的系统中,只看到BES 有明确的SLA的定义,通过这个SLA可以保证events 最低吞吐量。 consumer 的处理数量和处理速度 要与events 的产生的数量要匹配。

所以SLA的直接违反并不能导致系统的不可用,但是他由合同规定的惩罚其实也就宣判了系统的死刑。哪怕没有惩罚,由于SLA违反导致的系统堆积,也是会导致系统不能满足需求。

Unbounded Result set

很多OOM 就发生在这里,操作一个集合,要考虑他的大小。不能无限制增长。

如果你这集合一个query从数据库里查出,那么就应该加上limit size。 当这个数据表被插入大量数据时, 这个系统马上就因为load 太多数据崩溃。 所有的数据库查询都最好带上保护条件。

如果你这个集合是作为数据的一个容器, size 是固定的,那么当年你放入所有的数据后,需要用Collections.unmodifiableList()  之类方法来保护,防止有人无限加入数据

如果你这个集合是作为一个cache,动态的放入并且可以移除数据,那么你要考虑放入和移除 是不是结对出现的,甚至是原子性的。 如果不能保证, 就意味着你不能控制用户持续放入并且不清除。 那么在这种情况下就要考虑 用weak reference 或者soft reference。 在并发条件下, 这两个reference 必须加锁,否则就会出现死循环的现象。

WeakHashMap 有个出名的多线程bug, 用它必须用Collections.synchronizedMap 包装起来,否则会有如下的错误:

https://adambien.blog/roller/abien/entry/endless_loops_in_unsychronized_weakhashmap

 

    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
// not concurrent safy
                    prev = p;
                    p = next;
                }
            }
        }
    }

  

https://stackoverflow.com/questions/35534906/java-hashmap-getobject-infinite-loop

 https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6425537

作为cal transaction, 我们也要求加入start 必须 complete, 否则在raptor.io ,下面, start 的object  放入 容器,但是没有complete 会移除, 也会有OOM 的现象。



Service to operate files

有一些service 上上传下传文件,文件的操作涉及到上传多大的文件,和从文件系统取得多大的文件通常是NFS。 这些文件服务本身并不适合支持 http 协议的大流量的请求。这样的服务要特别监控流量和内存。 无论是文件太大,或者存储系统出现抖动, 都会导致系统急剧不稳定。

如果JVM 参数配置的不好,很容易让内存快速消耗。 这个时候要注意 配置年轻代和 metadataspace 的数值要比一般的大很多。




软件稳定的常见模式

Use Timeout

Timeout 是保护call 出去的service 长时间没有相应的情况, Slow response,SLA Resvision, Thread Block 都有一定的作用, 比如在timeout 过程中,通常会写一个 exception, 线上管理员 发现timeout增多,会找下游做相应的处理。 当error 达到一定程度, Service 会自动下线,不再调用。 这样的方式简单是简单了点,但是有一定的保护作用。

Circuit Breaker

在timeout 的基础上, 断路器模式会有所增强。Hystrix 和resilence4j 是nextflix 公司的不同年代对于断路器的实现。自动短路,自动开路。 这个模式在很多service里面得到了有选择的应用。

Servcie 用短路器 打开这个事件来进行markdown , service用hystrix 的异步command 来实现call 的异步调用。只是由于SOA /Ginger 有threalocal 的参数,所以他们必须采用seamphore的模式。理论上来说,自动markup 也可以用circuitbreaker close 事件来实现,但是对于production 来说,自动markup 比较危险,还是由线上管理员 来判定。

Bulkhead

船舱都是一块一块隔离的,一个船舱进水不会影响到另外一个船舱。那么系统的隔离如此,微服务的产生也是基于此。隔离到什么样的级别,什么样的服务需要在一起,什么样的服务需要分开。不是从软件代码的角度来看的, 完全是从业务的角度来划分。 比如秒杀,比如双十一,这些尖峰时刻的流量一定要重新考虑。


从代码的角度上看,如果数据库有partition, 如果不同类型的call所用的thread pool 有分开,如果context 都保存在threadlocal 那么当一部分发生错误的时候,不会影响另一部分。

Steady State (cache, logs, data purge)

需要保持系统进入一个稳定状态。对于时刻增长的数据必须进行控制。


比如cache 的大小, logs 必须用rolling log , 数据库的数据必须定期清除。否则系统负重前行,会逐渐压垮。 对也log,也不能事无巨细,都要记录。log 占用io 的开销,严重影响性能。 通常只有在warning 以上才会default 记录。如果要看更细的粒度, 通常会用一个开关来控制临时打开。


数据的清除大部分情况是和数据产生程序不是一个进程。也就是数据产生和清除是异步的。 

还有一种方式 内存对象的GC ,去清除数据。这个可以用phantom reference来关联删除。可以参考apache 的 FileCleaningTracker 

Fail Fast

Fail Fast 通常意味着校验。 如果一个输入本来就是不合法的,那就没必要把这个错误的参数带到别的服务中去了。直接错误就可以了。一个下游的service instance 不能正常工作,那么就没有必要把request 发到这个instance 上去了。

要实现fail fast, 有各种校验pattern。对于parameter 做一下input 校验,对于重要的模块, 通常会有heartbeat 或者 healthy endpoint 暴露给用户,方便他们容易拿到,进行fail fast.


比如 spring boot actuator ,  产品有netflix eureka。我们的pool 一个典型应用就是ECV check

Handshaking (TCP, Spring HATEOAS)

握手通常意味着有约定,有contract。 大家耳熟能详的TCP的三次握手。这是协议方面的, 在产品方面, 可以参考一个spring  HATEOAs, 先去握手,得到下一步应该调用的service endpoint。


通过握手协议 的时候交换校验,或者下一步信息,这样的信息交互是动态的。非常适合经常变化的场景。

另外一个经典的场景就是service discovery, 任何一个服务调用的时候都是一个别名,通过这个别名拿到真正的endpoint。一个endpoint 如果不能用了, 可以不用调整app的配置,来实现endpoint 的切换。 

Service discovery 和 HATEOAS 用法看起来相似,但是实际不同。 Service discovery 相对比教固定, HATEOAS 可以和flow 结合起来不同的flow 节点返回的信息不一样。

Test Harness (Traffic Mirror)

通常需要测试来发现问题,当系统有了新feature, 当系统需要更新底层框架,当系统需要更换OS的版本,这些都需要测试来保证改变不会带来问题。那么就需要一些黑盒的比较工具 比如service 的 traffic mirror, Bes 的bes mirror 这类工具, 不仅能够进行功能性测试, 还可以进行非功能性测试。


用合适的工具来保证系统的可测试性, 而不完全依赖单元测试,这类传统的白盒测试方案。 这类测试方案侵入性太高。 不适合大量的测试场景。

Decoupling middleware (Messaging)

用异步的方式通常可以减轻系统的直接压力。 对于messaging 系统, 当然也有大量的应用。 但是messaging系统通常是分布式的 他们测试有几个难点

  1. 控制某些event 发个特定的consumer

  2. 如何控制发布,让大量event 不会因为错误的consumer 逻辑出现site issue

  3. 如果获得大量的event 作为测试源

  4. 如何处理丢包

Retry

在service 调用失败, 有极大的可能是目标服务压力过大不能及时响应。 在出现异常的时候,进行retry 是有效可以避免服务失败。 messaging event 处理失败,如果有retry 机制,也能避免丢包。 但是retry 的重点是一定要能够控制次数。不能无限制retry。 retry 要么异步, 要么在同步下面能够快速完成。

Fallback

当系统拿不到理想的应用的时候, 降级通常也是合理的选择。 但是降级要分情况, 如果比较确定的情况,比如图象高清的传输速度慢, 降级成标清的。这是合理的。但是如果降级成本地配置等,一定要保证正确,如果过期了那还不如failfast。


福如东海

身体健康才能好事成双,好事赶上的多了, 自然是福如东海。那么什么指标能衡量身体健康。扩展性大致选取的指标


  1. TPS

  2. CPU

  3. Memory

  4. Latency

  5. GC Overhead

  6. Peak Traffic

扩展性越好,说明系统的跑的越快,自然也就身体越健康。


导致软件病恹恹的常见模式

Resource Contention

在资源竞争这个角度,如果过多的请求消费资源,那么就出现挤兑的现象。可以脑补医院挂号。既然是挤兑,总有抢的到, 也有抢不到的,对抢不到资源(比如 thread,db connection, file ..)那怎么办,只有等待了。 等待就意味着延时,不能及时响应。随着雪崩效应,资源请求阻塞越来越严重,造成了新的请求处理不了。这就是资源竞争。 在传统的infrastructure 模型中, 资源不够要么系统代码出问题了,需要紧急处理,要么就是加大资源,让它能够满足需求。 加大资源的情况,取决于系统的资源需求是不是无限增长。 跟现实生活一样,要么增加供给,要么减少需求。 如果是增加供给,那十万火急的时候,必须要快。这就是k8s 系统能够火爆三丈的原因。 比如下篇博文 线程的饥饿就是一个典型的例子 https://www.cnblogs.com/developernotes/p/14679509.html

 

Dependency flood

如果代码没有划分层次,随便加入依赖。那么就会造成依赖泛滥。 如果是在初始化过程中,需要不需要的都来初始化,造成启动过程超级缓慢。典型的例子就是v3。 另外,依赖过多,导致功能不能切分,想离婚可没那么容易。java 的问题就是哪怕我只用一个类,也要把那个jar 包全部引用过来,如果这个jar 包有啥autoconfig 之类的,就自然而然的启动了。 目前没有特别好的办法, raptor io 采用的 api 层的定义是OSGI 的一种简化版。从目前来看也是易用和解耦两方面结合的最好的。 


Handcrafted SQL

一个SQL写不好,没有利用到主键,index 当然会很慢。现在的问题是很多程序都用了data 访问的中间件, 比如一起的Hibernate ,系统用的DAL 这些中间件封装了复杂度,但是也需要用户根据中间件的特性来 变成可以高效执行的sql。 

Database Eutrophication 

数据库操作是一个很昂贵的操作,每次request 都操作DB 是个超级浪费的。 能不能较少数据库操作

  1. 几个独立的数据库查询合并一下

  2. 能不能利用cache 减少DB操作 比如系统 DAL 的RDS ,DAL 的code cache等等

对于DB需要parition 的情况, cache 就比较复杂。查询也比较复杂,看BES 的channel command会同时查找BES events 的所有 partition DB , 同时发到下一个channel去就是一个景点的例子,一般来说parition DB 查找如果有聚合那么就会导致系统变慢。


DB查询如果需要做数据库锁, 那么基本上就是select  for update. 这种如果表级别的锁不能长期持有,基本上是做快速的预定数据区间占有。 


数据操作太多,对于远程的用户来说,启动会变得超级慢。 本地开发效率很低。 我们更推荐用service的方式来提供数据。减少数据库的直接访问。


Failure spreading with retry

理想来说,有retry 是保证系统健壮的一个措施。但是如果retry写的不好,比如我们在bes 遇到的一个event,偶然的错误 会先向下游发出错误的event 之后再进入retry。 这样的处理retry 的次数越多,对系统的损害越大。当写入retry的逻辑的时候,一定要保证retry 即使出错,对逻辑也没有影响。 这种bug 通常会隐藏的很深。 需要从code review 之类的措施从源头把他消除。

DatatypeFactory Construct

DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar());

http://dimovelev.blogspot.com/2013/10/java-performance-pitfalls.html

在jdk xml的操作中,DatatypeFactory.newInstance() 是常见的用法,但是DatatypeFactory.newInstance() 在jdk的实现是一个非常昂贵的操作, 每次都调用这个方法会增加30% 的latency。 通常解决的办法是


用一个全局变量操作一次,每次都用这个instace 来调用。

Bad VM

差机器本身是个硬件问题。但是对于上游的service, 那就是软件问题。 上游的service性能不好。差的机器在机器少的时候,很容易发现。但是当一个pool 有大量机器,超过几百台,甚至1000台以上的时候, 找到一台或者几台坏机器靠肉眼观察就非常困难。即使有监控工具,也只能知道这几台机器性能比较差, 到底是什么配置原因,还是网络原因,还是机器比较差其实很难断定。程序员通常习惯从自己擅长的软件方面找原因,他们和硬件管理是两个团队。硬件管理通常也很难理解软件程序员到底找他们查什么,如果说sku,model 这些硬件管理一下子能反应过来。单纯说个性能差,很难有所共鸣。 但是如果沟通清楚,把部署在同一个地点的机器进行一些性能对比,如果有很大的差异,通常跟机器有关。有些机器,即使和他的机器型号相同,但是由于年代久远,处于退休边缘的话。也会表现很差。

Dependency instance failure

调用下游pool 的时候, 发现大部分调用性能正常,小部分性能缓慢,或者有异常。由于下游的pool是通过vip 暴露给用户,很难说是这个pool 如何了。这种情况大概率是一部分instance 不能正常工作了。这部分不能正常工作的instance 在pool 里占比很小,从pool 级别的monitoring 和 alerting 很难侦测到。 查询这些instance 可以通过cal log 的correlationid , 在下游的pool 中进行定位。 

那么就可以看到这些异常的instance。 作为分布式系统, request tracing 是非常重要的。

Synchronized block

多线程的系统如果每个request 都要经过同一个锁,那么这个锁就是瓶颈了。通常这样的情况系统就不再是内存消耗型,单纯比内存够不够已经不足以满足性能要求。这个时候通常我们认定这个系统是CPU 消耗型的,计算系统容量,CPU 的消耗是主要考虑因素。就算是内存还剩余很多,如果CPU 超过30% 就要考虑加机器了。


Synchronize 现在有很多种实现方式,没有哪一种好不好,只有合适不合适的场景。从并发性效率的角度上讲, 最好没有锁,但是从业务需求上讲,谨慎的选择合适的锁,是开发的时候要注意的。从简单的编程模型上看,锁是最简单解决问题的方式,但是在秒杀,大流量并发的时候,用锁就会严重影响性能,这个时候用分区,分表这种方式来减少流量, 用bit map 矩阵 的类CAS算法来减少锁的时间。

JVM parameter under size

常见的JVM 参数调整都是堆内存 -Xmx -Xms 或者 在一点1.8 一下的 PermSize

1.8 以上的metadataspace,从理论上来说这些参数越大越好,但是从实际情况来看, 这些参数越大,就会导致quota 要求越高。同经济性能来看,这些参数需要的越小越好。 那么如果才能使这些参数配置的恰到好处需要经验和观察。 如果系统配置的过小,明显可以看到当流量增大时,系统响应变慢,甚至引起频繁 fullgc 导致cpu 高, gc overhead 增大。


同时选择什么样GC collector 也是很重要,不同的GC 的策略是不同的,从目前来看,G1这种标记分代回收的是应用的比较广泛的,比CMS 等其他stop world 的时间要少。


Integration point latency

在一个facade 的系统中,比如signin,capacha 这些service 会被所有的系统调用, 而且调用的次数又很频繁。那么这个系统的性能就会影响面很大。但是,在老式的系统中, 尽管有这样的担忧,通常也不会把这种系统分散到各个用的到他的系统中。 V3 做了一些尝试,但是发现ownership, 发布时间,修改的影响都很难控制。在新的service mesh 的架构中,可以这么使用,但是从管理角度, 单独一个service 可能会更好。 那么就需要有足够的资源让他应付当前的和预见的流量。并且有monitoring 和alter 系统来监控他的performance. 让这个系统时刻响应时间符合SLA。


Log flood

Log 有几种,一种是硬盘的log ,一种是发到log service 的例如cal server. 不论哪种log,我们都要防止log 泛滥,事无巨细,都要report 带了了很多log 冗余,增加了系统的负担。


  1. 发向硬盘的log 基本的原则就是要发系统需要的信息,一般来说就是warning 以上的。而且log 文件要rolling 策略, 防止log 文件无限扩张。Batch 比较特殊,即使是一个batchjob 的log ,由于她要跑很长时间,那么一个batch log 大的有几G, 有时候会把磁盘占满。导致系统跑的很慢。这个时候就需要管理员/自动监控程序删除文件。

  2. 发向cal 的log 由于是用户自己写的,很难控制用户到底写什么有没有意义。 framework 会default enable 1% 的采样来减少后台系统的压力。

  1. 对于batch cal, 最大的问题不是又大又经常没有闭合,而是由于batch 跑的时间太长, 经常会出现batch cal trasaction 过期没有关闭的现象。所以我们建议batch尽量用cal event.

  2. cal transaction没有关闭会在zipkin 的data map 里留下冗余数据。在batch应用中,由于run万进程就结束了。所以问题不是很大,但是对于service 的应用, 如果cal transaction 没有关闭,就会造成内存泄漏。

 

GC 时间过长

如果young generation 设置的比较小,导致young GC 频繁,导致 进入老年代的速度加快。young GC 频繁,也会导致stop world时间增长。

如果有PhantomReference 对应的对象在老年代过多, 导致GC 时ref proces时间增长。 

提高系统性能的常见模式

Pool Connection

对于竞争的资源,比如connection,thread, 为了防止挤兑,引入了pool 的概念。如果资源够,就可以马上用,如果资源不够,那么需要等待有资源之后再用。这就避免了资源无限申请的弊端。但是这也可能带来系统的性能下降。 这就需要系统能够进行快速扩容。 用基础设施的scability 来解决软件的不足。



Use Cache carefully

使用cache 是很好的解决DB访问过多过频繁的问题。但是cache 也不是万能的它也是有使用范围。

不适合无限的数据,如果cache 的数据每次都不一样,每次都需要加入cache 这种情况就会导致cache 无限制增长。 那么在这种情况下,就需要做一些处理。

  • 是限制cache 的capacity,超出capacity的要么老的被踢出, 要么新的不能加入。

  • 用softreference 或者 weakreference ,利用GC 的时候回收多余的应用


WeakHashMap 有个出名的多线程bug, 用它必须用Collections.synchronizedMap 包装起来,否则会有如下的错误

https://adambien.blog/roller/abien/entry/endless_loops_in_unsychronized_weakhashmap



WeakReference 常见于用户自己的cache。


Soft Reference 最常见于Class, 在Class里有个关键的属性叫做reflectionData,这里主要存的是每次从jvm里获取到的一些类属性,比如方法,字段等。这个属性主要是SoftReference的,也就是在某些内存比较苛刻的情况下是可能被回收的,不过正常情况下可以通过-XX:SoftRefLRUPolicyMSPerMB这个参数来控制回收的时机,一旦时机到了,只要GC发生就会将其回收,那回收之后意味着再有需求的时候要重新创建一个这样的对象,同时也需要从JVM里重新拿一份数据,那这个数据结构关联的Method,Field字段等都是重新生成的对象。

https://mp.weixin.qq.com/s?__biz=MzIzNjI1ODc2OA==&mid=2650886992&idx=1&sn=97c6cd6cffa900b6979aba1a5438acd6&scene=21#wechat_redirect

Tune JVM

调整的参数,比如heap , metadataspace (1.8), 年轻代的大小都对系统的性能有很大的作用。


Heap size 就是 -Xmx -Xms 通常在系统应用中,都设置一样。 这个原因是当-Xms 不够用时候,JVM会吧Xms 慢慢加大, 知道和-Xmx 系统那么这个过程是需要stop world的,造成系统响应变慢,那么如果确定-Xmx 那么就一步到位减少调节的次数。


Metadataspace 前身是 pemsize,pemsize 在1.7 及以前是不会被收集的。那么这个系统的大小是固定的。在1.8 以后 这个区域也会被回收。但是这个相关的大小通常要看类的多少,在V3的情况下, 通常要设置1024M。


年轻代的大小 -Xmn 设置eden带的大小 在通常情况下,800M 是足够用了。 但是在有大文件交互的情况下,800M就会明显引起young GC 的频繁发生,通常这种情况下,我们把young 区设置能2G,就会大大提高系统的性能。平台迁移中的 GC 实践


https://zhuanlan.zhihu.com/p/111809384

https://mp.weixin.qq.com/s?__biz=MzIzNjI1ODc2OA==&mid=2650886992&idx=1&sn=97c6cd6cffa900b6979aba1a5438acd6&scene=21#wechat_redirect


https://docs.google.com/document/d/1LPkXDuyKhBzuDuzQQZTDuzlzntyL_D2RGt3X_OZDcMg/edit

Monitoring

对于系统的性能,要必须要有监控的功能,通过监控来进行反馈调节。谈到监控,就必须要有数据。数据要从引用里来,那么在应用里必须就有metrics 的受的模块。对于系统性能在raptor.io里就有 micrometer,外部调用服务有acturator。  在老的v3 有perform , 有专门的calhearbeat 。 

App的应用的逻辑新能有cal 


那么这些综合起来就可以知道一个系统运行的怎么样,性能有没有下降。同样根据这些metric ,可以利用sre 的工具对系统进行在线的调节。


除了系统工具,在infrastcture 方面同样有event 报告出来,那么同样可以进入到监控系统来进行处理。 有些是自动,有些需要人工介入。 

Code Review

Code Review 是最好的预防不好的代码进入的方式, 但是它的难点是对review 的人提出了很高的要求。

  1. Reviewer 的人要懂业务,知道被review的代码在解决什么问题

  2. Reviewer 要有能力给出代码优化的方案

  3. Reviewer 要有责任心对待review 的代码认真仔细的看。

  4. Reviewer 要有权威性,给出不同意的原因能被大家接受。

 

一次两次比较容易,但是长期坚持是不太容易的。这就需要建立有效的机制和process。 通常code review 做的最好的是open source 项目,有严格的流程和条件。

Performance Analysis

性能分析最终要落实到code ,有些情况是比较容易找到哪里比较慢的。比如通过cal,你可以看到每一步发生的时间,来看具体到那一步有问题。 Performance 常见的问题前面谈了很多。具体的问题需要具体分析。

在OOM 的情况下,性能通常要看Heapdump 来分析最大object 从来来判断这个Object是哪段代码带来的来判断。这种情况通常可以通过可memory 慢慢下降来判断。

在Thread lock 情况下, 性能通常要看threaddump 来分析死锁的thread。 这种可以看到cpu usage 慢慢上升来判断。

Necessary initialization

server 启动慢,是v3 的用户最抱怨的问题,尤其是中国的用户。一方面是数据连接过多,一方面是启动模块过多,不管用的到用不到, 反正一股脑都跑一遍。如何让系统能够只跑自己用的到的,这是个有意思的事情。对于启动我们基本上可以这么分类


  1. 基础设置的启动。这个不管怎么样都是必须的。比如configbean,cal,esam. 而且必须保证这些先启动。

  2. App 逻辑相关的,这个app启动必须要的。

  3. App 逻辑无关的,但是通过autowire 进来的,或者通过自定义进来的。

我要避免的就是#3。道理上希望每个启动模块都有条件检测。但是这个通常很困难。 raptor.io给了一个方案,为了防止类依赖带入很多不必要的启动模块,那么component 只允许依赖api 层,尤其是一些conditionalonClass 的类,这些条件要慎重使用,因为一有类的依赖就会启动。

 Log Policy and Sampling

Log policy /Sampling 本质上是解决log 过多,尤其是无效的log 过多的问题。基本的原则是有错误的肯定要记全,无错的要采样。所有的cal ,必须要能够完整闭合。 


从cal 必须完整闭合的角度,通常不会有问题,如果有问题就用户也会看的出来。

Warm up

在系统初始话好,有些service 第一次调用需要花很长的时间,比如trading api ,要load wsdd 之类的系统配置。有些jsp 访问需要预编译。 在这样的情况下,为了提高第一次的访问速度,会用warm up 机制来预热。在raptor/raptor.io 都有warmup 功能。 

Infrastructure


 


这张图左一是传统的模型,基于虚拟机的架构,比较重量级。 右一是kubernetes 基于docker 的实现,轻量快速是当前最火的基础架构。基本上所有的公司都在向这个架构迁移。在k8s 的基础上, 出现了service mesh, serverless 之类的架构。 在这个架构的基础上,能够在infrastructure 的级别来实现软件所打不到的scability 的功能。


无功受禄

天上掉钱是最好的无功受禄的解释。就是活的顺心, 各种维护不仅轻松,而且省钱。这需要两外面的因素,一个是数据,一个是流程。 数据是决策的依据,流程是决策的风险控制。决策和流程通常希望越自动化越好,越只能越好。但是从目前来看, 现有的人工智能还达不到专业人士的能力,那么只能部分靠工具,部分靠人。这部分通常不是我们的工作职责,所以我简单列一下。





Responsibility


  • Monitoring the health of the system and quickly restoring service

  • Tracking down the cause of problems

  •  Keeping software and platforms up to date, including security patches

  • Keeping tabs on how different systems affect each other,

  • Anticipating future problems and solving them before they occur (e.g., capacity planning)

  • Establishing good practices and tools for deployment, configuration management, and more

  • Performing complex maintenance tasks, such as moving an application from one platform to another

  • Maintaining the security of the system as configuration changes are made

  • Defining processes that make operations predictable and help keep the production environment stable

  • Preserving the organization’s knowledge about the system, even as individual people come and go 

Approach

  • Providing visibility into the runtime behavior and internals of the system, with good monitoring

  • Providing good support for automation and integration with standard tools

  • Avoiding dependency on individual machines (allowing machines to be taken down for maintenance while the system as a whole continues running uninterrupted)

  • Providing good documentation and an easy-to-understand operational model (“If I do X, Y will happen”)

  • Providing good default behavior, but also giving administrators the freedom to override defaults when needed

  • Self-healing where appropriate, but also giving administrators manual control over the system state when needed

  • Exhibiting predictable behavior, minimizing surprises 

Simplicity: Managing Complexity

 打造三星高照的系统,需要花很多精力。这个APP内在和PDLC都需要简单明了。这个技术团队要当魔术师,复杂的内部,外部是超级简单。下面是简要几点



•Does not necessarily mean reducing its functionality

•Find good abstractions  

•Tools

•Tools Integration

•AIOps VS DevOps

•Platform changes world


只有点点滴滴的事情做好,才能打造出三星高照的系统。任何一个看起来炫酷的地方,其实都还是最基本的原则的应用。



 

posted @ 2021-04-19 15:59  程序员札记  阅读(357)  评论(0)    收藏  举报