0%

cover

第一部分 概述

1. 交易型系统设计的一些原则

1.1 高并发原则
  • 无状态
  • 拆分(系统维度、功能维度、读写维度、AOP 维度、模块维度)
  • 服务化(进程内服务 - 单机远程服务 - 集群手动注册服务 - 自动注册和发现服务 - 服务的分组/隔离/路由 - 服务治理如限流/黑白名单)
  • 消息队列
  • 数据异构(数据异构、数据闭环)
  • 缓存银弹(浏览器缓存、App 客户端缓存、CDN 缓存、接入层缓存、应用层缓存、分布式缓存)
  • 并发化
2. 高可用原则
  • 降级
  • 限流
  • 切流量
  • 可回滚
3. 业务设计原则
  • 防重设计(防重 Key、防重表)
  • 幂等设计(消息中间件)
  • 流程可定义
  • 状态与状态机
  • 后台操作系统可反馈
  • 后台系统审批化(操作日志记录,保证操作可追溯、可审计)
  • 文档和注释
  • 备份

第二部分 高可用

2. 负载均衡与反向代理

对于一般应用来说,有 Nginx 就可以了,但 Nginx 一般用于七层负载均衡,吞吐量有一定限制,为了提升整体吞吐量,会在 DNS 和 Nginx 之间引入接入层,如使用 LVS(软件负载均衡器)、F5(硬负载均衡器)可以做四层负载均衡,即首先 DNS 解析到 LVS/F5,然后 LVS/F5转发给 Nginx,再由 Nginx 转发给后端 Server。

负载均衡主要关注几个方面:

  • 上游服务器配置:使用 upstream server 配置上游服务器
  • 负载均衡算法:配置多个上游服务器时的负载均衡机制
  • 失败重试机制
  • 服务器心跳检查

负载均衡算法 :

  • round-robin:轮询算法,默认 LB 算法,配合 weight 配置可以实现基于权重的轮询
  • ip_hash:根据 IP 进行 LB,相同的 IP 将 LB 到同一个upstream server
  • hash key:对某一个 key 进行 hash或者使用一致性 hash 算法进行 LB
  • least_conn:将请求 LB 到最少活跃连接的 upstream server
  • least_time:Nginx 商业版功能,基于最小平均响应时间进行 LB

健康检查:

  • TCP 心跳检查
  • HTTP心跳检查

3. 隔离术

隔离是指将系统或资源分割开,系统隔离是为了在系统发生故障时,能限定传播范围和影响范围,即发生故障后不会出现滚雪球效应,从而保证只有出问题的服务不可用,其他服务还是可用的。资源隔离通过隔离来减少资源竞争,保障服务间的相互不影响和可用性。出现系统问题时,可以考虑 LB 路由、自动/手动切换分组或者降级等手段来保障可用性。

  • 线程隔离(不同的线程池)
  • 进程隔离(单实例到多子系统)
  • 集群隔离(不同的集群)
  • 机房隔离(不同的机房)
  • 读写隔离(主从模式等)
  • 快慢隔离
  • 动静隔离(CDN)
  • 爬虫隔离
  • 热点隔离(秒杀、抢购做成独立系统或服务隔离,对于读热点,可以使用多级缓存,对于写热点,可以使用缓存+队列模式削峰,)
  • 资源隔离
  • 环境隔离(测试环境、预发布环境、灰度环境、正式环境)
  • 压测隔离(真实数据、压测数据)
  • AB 测试
  • 缓存隔离
  • 查询隔离(简单、批量、复杂条件查询分别路由到不同集群)
  • 使用 Hystrix 隔离
  • 基于 Servlet 3 实现请求隔离

4. 限流

一般幵发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、 限制瞬时并发数(如Nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间 窗口内的平均速率(如Guava的RateLimiter、Nginx的limit_req模块,用来限制每秒的 平均速率),以及限制远程接口调用速率、限制MQ的消费速率等。另外,还可以根据 网络连接数、网络流量、CPU或内存负载等来限流。

限流算法:

  • 令牌桶算法
  • 漏桶算法
  • 计数器算法

应用级限流:

  • 限流总并发、连接、请求数(限制 TPS、QPS)
  • 限流总资源数(池化,如数据库连接池、线程池)
  • 限流某个接口的总并发、请求数(使用AtomicLong或者Semaphore进行限流,Hystrix)
  • 限流某个接口的时间窗请求数(如使用Guava Cache来存储计数器)
  • 平滑限流某个接口的请求数(Guava RateLimiter)

分布式限流

分布式限流最关键的是要将限流服务做成原子化,解决方案可以使用 Redis+Lua 或者 Nginx+Lua 技术进行实现

接入层限流

接入层通常指请求流量的入口,主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、降级、限流、A/B测试、服务质量监控等。
Nginx接入层限流可以使用 Nginx 自带的两个模块:连接数限流模块 ngx_http_limit_conn_module 和漏桶算法实现的请求限流模块 ngx_http_limit_req_module,或者 OpenResty 提供的 Lua 限流模块 lua-resty-limit-traffic

节流

防止多个相同事件连续重复执行,主要有throttleFirst、throttleLast、throttleWithTimeout

5. 降级特技

降级的最终目的是保证核心服务可用,即使是有损的。
降级需要根据系统的吞吐量、响应时间、可用率等条件进行手工降级或自动降级。

5.1 降级预案

自动开关降级/人工开关降级,读服务降级/写服务降级,多级降级,页面降级/页面片段降级/页面异步请求降级/服务功能降级/读降级/写降级/爬虫降级/风控降级

5.2 自动开关降级
  • 超时降级
  • 统计失败次数降级
  • 故障降级
  • 限流降级
5.3 人工开关降级
5.4 读服务降级
5.5 写服务降级
5.6 多级降级
  • 页面 js 降级开关
  • 接入层降级开关
  • 应用层降级开关
5.7. 配置中心(通过配置方式动态开启/关闭降级开关)
  • 应用层 API 封装
  • 使用配置文件实现开关配置
  • 使用配置中心实现开关配置
5.8. 使用 Hystrix 实现降级
5.9. 使用 Hystrix 实现熔断

6. 超时与重试机制

代理层超时与重试

如Haproxy/Nginx/Twemproxy,需设置代理与后端真实服务器之间的网络连接/读/写超时时间

Web 容器超时

提供HTTP服务运行环境的,如Tomcat/Jetty,需设置客户端与容器之间的网络连接/读/写超时时间,和在此容器中默认 socket 网络连接/读/写超时时间

中间件客户端超时与重试

如 Dubbo、MQ、HttpClient 等,需设置客户的网络连接/读/写超时时间与失败重试机制

数据库客户端超时

如MySQL/Oracle/PG,需要分别设置JDBC Connection、Statement的网络连接/读/写超时时间,事务超时时间,获取连接池等待时间

NOSQL客户端超时

如Mongo、Redis,需要设置其网络连接/读/写超时时间,获取连接池等待时间

业务超时

如订单取消任务、超时活动关闭,还有如Future#get(timeout, unix)限制某个接口的超时时间

前端Ajax超时

浏览器通过Ajax访问时的网络连接/读/写超时时间

总结:

最重要的是网络相关的超时设置。
超时后应该有相应的处理策略,如重试(稍后再试、尝试其它分组服务、尝试其它机房服务)、摘掉不存活节点(负载均衡/分布式缓存场景下)、托底(返回历史数据/静态数据/缓存数据)、等待页或错误页。
对于非幂等写服务应避免重试,可以提前生成唯一流水号保证写服务操作通过流水号来实现幂等操作。
在进行DB/缓存服务器操作时,需经常检查慢查询,同时在超时严重时,可以直接将该服务降级。
对于有负载均衡的中间件,考虑配置心跳/存活检查。

7. 回滚机制

回滚是指当程序或数据出错时,将程序和数据恢复到最近的一个正确版本的行为。常见的如事务回滚、代码库回滚、部署版本回滚、数据版本回滚、静态资源版本回滚等。

7.1 事务回滚

单库事务回滚直接使用相关 SQL,分布式数据库可以使用分布式事务,如两阶段提交、三阶段提交协议。另外可以考虑入事务表、消息队列、补偿机制(执行/回滚)、TCC 模式(预占/确认/取消)、Sagas模式(拆分事务+补偿机制)等实现最终一致性。

7.2 代码库回滚

Git/SVN

7.3 部署版本回滚

部署版本化、小版本增量发布、大版本灰度发布、架构升级并发发布

7.4 数据版本回滚

设计版本化数据结构时,有全量和增量两种思路

7.5 静态资源版本回滚

全量新版本保证版本可追溯。清理CDN 缓存,在新 URL上添加随机数并清理浏览器缓存。请求参数加上版本号

8. 压测与预案

在大促来临之前,研发人员需要对现有系统进行梳理,发现系统瓶颈和问题,然后 进行系统调优来提升系统的健壮性和处理能力。一般通过系统压测来发现系统瓶颈和问 题,然后进行系统优化和容灾(如系统参数调优、单机房容灾、多机房容灾等)。即使 己经把系统优化和容灾做得非常好了,但也存在一些不稳定因素,如网络、依赖服务的 SLA不稳定等,这就需要我们制定应急预案,在出现这些因素后进行路由切换或降级处 理。在大促之前需要进行预案演习,确保预案的有效性。

8.1 系统压测

一般指性能压力测试,评估系统的稳定性和性能,通过压测数据进行系统容量评估,决定是否需要扩容和缩容。压测要有压测方案 {如压测接口、并发量、压测策略(突发、逐步加压、并发量)、压测指标(机器负载、QPS/TPS、响应时间)},之后产出压测报告{压测方案、机器负载、QPSTPS、响应时间(avg、min、max)、成功率、相关参数(JVM参数、压缩参数)等},根据压测报告分析的结果进行系统优化和容灾。

  • 线下压测(使用Jmeter、Apache ab 压测某个接口或某个组件(如DB 连接池),然后进行调优,实现单个接口或组件性能最优。线下压测环境和线上不同,适合组件级压测,数据只能参考)
  • 线上压测(按读写分为读压测、写压测、混合压测,按数据仿真度分为仿真压测和引流压测(如TCPCopy),按是否给用户提供服务分为隔离集群压测和线上集群压测。注意离散压测(选择的数据应该是分散的或长尾的)和全链路压测)
8.2 系统优化和容灾

拿到压测报告后,接下来分析报告,然后进行有针对性的优化,如硬件升级、系统扩容、参数调优、代码优化、架构优化(如加缓存、读写分离、历史数据归档)等,根据压测数据因地制宜地解决。在系统优化时,要进行代码走查,发现不合理的参数配置,如超时时间、降级策略、缓存时间等。在系统压测时进行慢查询排查,如Redis、MySQL等。在应用系统扩容方面,根据往年流量和运营业务方沟通,评估是否需要扩容及扩容容量。扩容时考虑系统容灾,如分组部署、跨机房部署等,保证高可用。

8.3 应急预案

在系统压测后会发现一些系统瓶颈,在系统优化之后会提升系统吞吐量并降低响应时间,容灾之后的系统可用性得以保障,但还存在一些风险,如网络抖动、某台机器负载过高、某个服务变慢、DB load值过高等,为了防止因为这些问题导致系统雪崩,需要制定应急预案。

应急预案可分几步执行:

  • 系统分级(划分交易核心系统和交易支撑系统,对不同级别的系统实施不同的质量保障)
  • 全链路分析(从用户入口到后端存储,梳理出关键路径,进行评估和预案,防止问题的级联效应和雪崩效应)
  • 配置监控报警
  • 制定应急预案

最后,要对关联路径实施监控报警,包括服务器监控(CPU使用率、磁盘使用率、网络带宽等)、系统监控(系统存活、URL 状态/内容监控、端口存活等)、JVM 监控(堆内存、GC 次数、线程数等)、接口监控(接口调用量『每秒/每分钟』、接口性能『TOP50/TOP99/TOP999』、接口可用率等)。然后配置报警策略,如监控时间段、报警阈值、通知方式等。在报警后要观察系统状态、监控数据或者日志来查看系统是否真的存在故障,如果确实是故障,则应即及时执行相关预案处理,避免故障扩散。

第三部分 高并发

9. 应用级缓存

9.1 缓存简介

让数据更接近于使用者,目的是让访问速度更快。

9.2 缓存命中率

从缓存中读取数据的次数与总读取次数的比率,命中率越高越好。
缓存命中率 = 从缓存中读取次数/总读取次数(从缓存中读取次数+从慢速设备上读取次数)

9.3 缓存回收策略
  • 基于空间:设置缓存存储空间,超过空间上限时回收
  • 基于容量:设置缓存最大数量大小
  • 基于时间:TTL(Time To Live 存活期),TTI(Time To Idle 空闲期)
  • 基于 Java 对象引用:软引用,弱引用
  • 回收算法:
    • FIFO(First In First out 先进先出算法)
    • LRU(Least Recently Used 最近最少使用算法)
    • LFU(Least Frequently Used 最不常用算法)
    • 实际应用中基于 LRU 的缓存居多,如 Guava Cache、Ehcache 等
9.4 Java 缓存类型
  • 堆缓存:使用Java堆内存来存储缓存对象,好处是没有序列号/反序列化,速度最快。缺点是缓存数据量很大时,GC 暂停时间会变长,存储容量受限于堆空间大小,一般通过软引用/弱引用来存储缓存对象,这样堆内存不足时可以强制回收这部分内存。一般存储较热的数据。
  • 堆外缓存:存储在堆外内存,可以减少 GC 暂停时间,可以支持更大的缓存空间,但读取数据需要序列化/反序列化,速度较慢。
  • 磁盘缓存:缓存存在磁盘上,在 JVM 重启时缓存还在,而堆缓存/堆外缓存会丢失,需重新加载。
  • 分布式缓存:与上面的进程内缓存和磁盘缓存不同,多 JVM 实例。可以使用Redis/Ehcache-clustered等实现。

    两种模式:
    * 单机时:存储最热的数据到堆缓存,相对热的数据到堆外缓存,不热的数据到磁盘缓存
    * 集群时:存储最热的数据到堆缓存,相对热的数据到堆外缓存,全量数据到分布式缓存
    
9.5 缓存使用模式

主要分两大类:Cache-Aside 和 Cache-As-SoR(Read-through、Write-through、Write-behind)

三个名词

  • SoR(system-of-record):记录系统,又叫数据源,即实际存储原始数据的系统
  • Cache:缓存,是 SoR 的快照数据,访问速度比 SoR快,放入 Cache 目的是提升访问速度,减少回源到 SoR 的次数
  • 回源:即回到数据源头获取数据,Cache 没有命中时,需要从 SoR 读取数据。

缓存使用模式:

  • Cache-Aside:业务代码围绕着 Cache 写,是由业务代码直接维护缓存。读场景,先从缓存获取数据,如果没有命中,则回源到 SoR 并将源数据放入缓存供下次读取使用。写场景,先将数据写入 SoR,写入成功后立即将数据同步写入缓存。适合使用 AOP 模式实现。
  • Cache-As-SoR:把 Cache 看作为 SoR,所有操作都是对 Cache 进行,然后 Cache 委托给 SoR 进行真实的读写。业务代码中只看到 Cache 的操作,看不到关于 SoR 相关的代码。
  • Read-Through:业务代码首先调用 Cache,如果不命中由Cache回源到 SoR。需要配置一个 CacheLoader 组件用来回源到 SoR 加载源数据。
  • Write-Through:穿透写模式/直写模式,业务代码首先调用 Cache 写数据,然后由 Cache 负责写缓存和写 SoR,而不是由业务代码。需要配置一个 CacheWriter 组件用来回写 SoR。
  • Write-Behind:也叫 Write-Back,回写模式。不同于Write-Through是同步写 SoR 和 Cache,它是异步写,异步之后可以实现批量写、合并写、延时和限流。
  • Copy pattern:有 Copy-On-Read(在读时复制) 和 Copy-On-Write(在写时复制)两种。
9.6 性能测试

使用 JMH 进行基准性能测试。首先进行 JVM 预热,然后进行度量,产生测试结果。

10. HTTP 缓存

10.1 HTTP 缓存简介

当用浏览器访问网页或 HTTP 服务时,根据服务器端返回的缓存设置响应头将相应内容缓存到浏览器,下次可以直接使用缓存内容或者仅需要去服务器端验证内容是否过期即可。减少浏览器与服务器端之间来回传输的数据量,节省带宽以提升性能。

10.2 HTTP 缓存
  1. 服务器端响应的 Last-Modified 会在下次请求时,将 If-Modified-Since 请求头带到服务器端进行文档是否修改的验证,如果没有修改就返回304,浏览器可以直接使用缓存内容
  2. Cache-Control:max-age和 Expires 用户决定浏览器端内容缓存多久,即多久过期,过期后则删除缓存重新从服务器端获取最新的。
  3. HTTP/1.1规范定义的 Cache-Control 优先级高于 HTTP/1.0规范定义的 Expires
  4. 一般情况下 Expires=当前系统时间+缓存时间(Cache-Control:max-age)
  5. HTTP/1.1规范定义 ETag 为“被请求变量的实体值”,可简单理解为文档内容摘要,ETag 可用来判断页面内容是否已经被修改过了。
10.3 一些经验
  • 只缓存200状态码的响应,像302等要根据实际场景决定,比如当系统出错时,自动302到错误页面,此时缓存302就不对了
  • 有些也没不需要强一致,可以进行几秒的缓存。比如商品详情页展示的库存,可以缓存几秒钟。短时间的不一致对于用户来说没有影响的
  • JS/CSS/image等一些内容缓存时间可以设置为很久,比如1月甚至1年,通过在页面修改版本来控制过期
  • 假设商品详情页异步加载的一些数据,使用 last-modified 进行过期控制,而服务器端做了逻辑修改,但内容是没有修改的,即内容的最后修改时间没变。若果想过期这些异步加载的数据,则可以考虑在商品详情页添加异步加载数据的版本号,通过添加版本号来加载最新的数据,或者将 last-modified 时间加1来解决,但这种情况下使用 ETag 是更好的选择
  • 商品详情页异步加载的一些数据,可以考虑更长时间的缓存,比如1个月而不是几分钟。可以通过 MQ 将修改时间推送到商品详情页,从而实现按需过期数据
  • 服务器端考虑使用 tmpfs 内存文件系统缓存、ssd 缓存,使用服务器端负载均衡算法一致性哈希来提升缓存命中率
  • 缓存 key 要合理设计。比如去掉某些参数或排序参数,以保证代理层的缓存命中率;要有清理缓存的工具,出问题时能快速清理掉问题 key。
  • AB 测试/个性化需求时,要禁用掉浏览器缓存,但要考虑服务器端缓存
  • 为了便于查找问题一般会在响应头中添加源服务器信息,如访问京东商品详情页会看到 ser 响应头,此投存储了源服务器 IP,以便出现问题时,知道哪台服务器有问题

11. 多级缓存

缓存相关的问题有很多,如缓存算法、热点数据与更新缓存、更新缓存与原子性、缓存崩溃与快速恢复等。
多级缓存是指在整个系统架构的不同系统层级进行数据缓存,以提升访问效率。

典型的应用整体架构和流程如图:

Pasted Graphic 1.png

元宵节,又名上元节、灯节、小正月,在我们老家的方言叫做过十五。

过十五算是第二大节日了,大概因为这一天很大程度上分担了清明节的一些事宜。这天的白天,家家户户男女老少们全家出动,拿上铁锹扫帚,带上烟花爆竹,去修缮修缮祖坟,除一除草,打扫打扫卫生。如果祖坟是土坟的话,还在附近用铁锹挖一顶“官帽”,戴在坟墓的顶上。等打扫的差不多的时候,烧纸的开始烧纸,放炮的放炮,烟花爆竹一阵噼里啪啦,子子孙孙齐齐跪下,对着一座座坟墓叫着陌生的称呼。

这一天,上述的事情大多要反复执行几次,比如谁谁谁他爷爷的,谁谁谁他奶奶的,他爷爷的爸爸的,他爷爷的妈妈的,他爷爷的爷爷的,他爷爷的奶奶的… 因为家户不大,大多也就只能追溯到往上4、5代。

如此这般,白天的任务算是结束了,到了夜晚,才是过十五的高潮开始。等到六七点钟天黑的时候,家家户户又张起了灯,往祖坟前送去,俗称“送灯”,送灯大概的意思是为祖先们照亮回家的路。

如果说除夕的时候,只是断断续续打持久战般地,不停有人家放烟花,那么过十五就是密集的轰炸战,所有人家都拿出自家所有的烟花爆竹,然后一个劲地在祖坟前放掉。

这一晚的这个时候,黑夜如白昼,整个世界都是一个大的烟花展,到处是五颜六色各式各样的烟花,一家放的比一家多,一家放的比一家好看。直放到耳朵起茧子,脑中有回声,眼前一片花,空气中爆竹气味超标到无法呼吸。

这一天也是小孩子们快乐的一天,在此之前他们已经置备好了灯笼,有的是街上买的有电灯有音乐的,有的是买的装蜡烛点亮的,实在没有买的就自己用白酒盒子做一个灯笼,然后里面插上蜡烛。等到黑夜来临的时候,他们纷纷点上蜡烛,然后提着灯笼到处乱窜。嚯,谁的灯笼有跑马灯啦,谁的灯笼会唱歌啦,谁的灯笼烧着了啦。

等到夜开始深的时候,爆竹声也渐渐低了下来,偶尔的一两声爆竹,在空荡荡的乡下反复地回声着,显得特别落寞。人们陆续回家,宣告过十五的结束。也宣告着春节的结束。

我还挺怀念那个背着铁锹去墓地里上坟,闻着满鼻子的爆竹味打着灯笼到处跑的日子。可回想起来,自从上大学后,就再也没有在家里过元宵节。

元宵节到了现在,索性快要成为一个汤圆节,说起元宵节除了吃汤圆好像也不知道要做什么。
那么为什么会成为这样呢?我猜想有以下几个原因:

  1. 过节不放假。不放假的节日都是耍流氓,上班都努力工作去了,谁还记得节日?(手动狗头)
  2. 没有宣传。比如商场超市没有像端午中秋圣诞那种促销活动和宣传,导致大众对元宵节的感觉越来越淡。
  3. 时间点有点尴尬。刚过完春节,大家有的还没从假日综合症中走出来,有的正是分道扬镳各奔东西的时候,元宵节的团圆也变得很难团圆。

但愿那个把春节延长到元宵节的提议,早日被采纳实施吧!

ddd

2013 年 6 月,故事的一切,看起来都很美好。

毕业一两年的我,正在第一家公司里贪婪地成长着,最近两个月刚刚得到提拔,负责公司新兴的业务。这一年,女朋友(现在的老婆)来到我在的这个城市一起生活,事业和爱情都飞速地发展着。

忽然有天听到一个消息:隔壁工位同部门的小兄弟江波要离职了,我有些愕然。江波是另一个项目的核心负责人之一,小小的个头隐藏着大大的能量。在我们同事之间平日里大言不惭地键盘侠、嘴炮党时,他常常发挥党员作用,说一些能让我们马上安静下来的道理。用现在的话说:他有一种老干部式的智慧。也因为他的老干部身份,每逢一些国际国内波动、社会变革、经济动荡的大议题出现时,我们都会争取让江波加入讨论之中,虽然…他经常嗤之以鼻。

江波在公司里他很低调,话也不多,每天上班准时背着运动双肩包出现,然后打一保温杯开水,开始一天的工作,等到下班又准时收拾双肩包走人。除非赶项目或者迫不得已,很少看到他在工位上加班。但他仍然是部门老大最喜爱的员工之一,他懂老大,老大也懂他,公认的前途无量。

我和江波工位虽处隔壁,但工作上并无什么交集,只有一次业务对接的时候,他的专业性让我敬佩不已,甚至很直接地指出了我工作任务中的一些问题,有些问题连我自己都没有想清楚过。从此更是对他暗暗多了几分佩服。

2013 年是移动互联网发展如火如荼的一年,尽管身处传统公司的我们很难感受到外面的互联网世界快速发展,但江波平日里最喜爱看 36kr 和 YC。在看多了他的这些举动后,我也暗暗地去关注这些,想去了解它们为何有如此魅力,不觉间也被带入了移动互联网的大浪潮里。

drink

送别江波的那天,部门里的所有成员都出席了送别宴。过去六年了,我还能记得那天的场景,昏黄的灯光,拥挤的位置,一杯接一杯的啤酒,和微醺的我们。大家言语不多,嘴里只是反复吐出一个字:干。

那一天,老大情绪有些激动,喝的最多。酒至深处之时,老大呜咽着感叹道:“我不知道为什么,有时候怎么这么难呢!”。大家听闻此言,顿时有些感伤,竟不知说些什么。江波也受了影响有些伤感,拍了拍老大的肩膀。

那时,我挤在人群之间,不会喝酒却在这一晚喝的有些迷糊。年轻的我,意气风发的我,看到这离别的场景,郁郁寡欢的人群,伤感的老大,我是错愕的,甚至有些想哑然失笑。老大啊老大,你三四十的人了,有家有室,风风雨雨也历经不少了,为何一个下属离职就让你哭成这样。

这一晚,大家没有谈到江波的未来,江波也没有主动交代,只依稀记得听人说有他去投入到创业大潮中的,有说他去互联网巨头公司的,有说他回老家的。

宴会结束后,很少喝酒的我带着一身酒气一个人走回了家,女朋友问为何今天破例喝酒,答曰:有个很好的同事离职了,我们一起送别他。

江波走后,很快大家便把江波的工作分而食之,好像江波从来没有在过一样。连他的工位,也有另一个同事搬了过来,至今想起那个位置,一半的记忆是江波的,另一半的记忆是后面那个同事的。

2013 年在移动互联网的滚滚大潮中落下帷幕,2014 年是 O2O 和社交爆发的一年。江波离职的一年后,我也按捺不住,一头扎入了移动互联网的大浪潮中。再后来,浮浮沉沉,跌跌撞撞,成功过,失败过,团结后,孤独过,聚过,散过,相识过,相忘过…

现在,我已经快要忘记了江波,也快要忘记了那个老大,那个团队。活在当下,过去和历史就是一段虚无的记忆,除了回味,你无法证明它是否真的发生过。

除非…偶然翻起过去的老照片。当我看到一张张老照片,一张张熟悉的面孔,我才知道,这些东西都没有被遗忘,只是它们被压缩在大脑的最深处,当再次解压的时候,所有那些记忆都会释放迸发出来。

memory

我又想起了老大的那次流泪,这次,我也有些伤感起来,甚至想流泪。当我想象自己是 2013 年的老大时,我哭了出来。2013 年到底是老大哭了,还是我哭了,他的流泪到底是他真的流泪,还是只是我现在的想象?

笑,世界陪你笑;哭,你一个人哭。

我曾以为江波走后,总会回来看我们的,直到我离开公司的时候,也没有见过他,甚至听不到他的任何消息。在我离开公司后,我知道人生路很长,却只有往前,没有向后看,前方的路已经够眼花缭乱,哪还有精力常回头看看。

人生苦短,相遇相识相知很难,此去一别,也许从此天各一方,再也不见。这大概就是老大那次流泪的原因吧。

bye

后记:2013 年后至今,我再也没有联系过江波,更没见过他,也没有从别人那里打听到他的消息。2014 年后,我再也没有见过老大。某个深夜,我还是很想他们。

cover
As we know,after iOS 10,Apple launched Notification Service Extension,Looking at the doc,the description of UNNotificationServiceExtension is An object that modifies the content of a remote notification before it's delivered to the user.,that is, in a Before the remote notification is displayed to the user, the notification can be modified throughUNNotificationServiceExtension.

Stop talking nonsense and get down to business.

一、How to monitor received push notifications

  1. Create a new Target on the original Project, Select Create
    image-20190304210307444

  2. Add a new field mutable-content": "1" to the content pushed by the server,,at the same level as alert/badge/sound。 For example, when testing push in the push platform, the settings are as follows:image-20190304212029192

  3. Run the main Target of the Project, then run Notification Service Extension,and select the main Target

  4. Send test push,then you can see it execute and enter - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler

二、How to statistics

  1. If you are connecting a 3rd push platform, you can check whether it supports. For example: Jiguang API: Notification Service Extension

  2. If you implement it yourself, there are two options:

    1. Write network requests in the notification extension project and send the push arrival data to the backend server
    2. Save the push arrival data to the local App through App Group, and then process it in the main Target

三、How to use App Groud to share data

  1. Login to https://developer.apple.com ,Create App Group

  2. Configure in project,target - Capabilites - App groups

  3. Use in code

    1. Use in NSUserDefaults

      1
      2
      3
      4
      5
      6
      NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.company.appGroupName"];
      // write data
      [userDefaults setValue:@"value" forKey:@"key"];

      //read data
      NSLog(@"%@", [userDefaults valueForKey:@"key"]);
  1. Use in NSFileManager

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // write data
    NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.domain.groupName"];
    NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"fileName"];

    NSString *text = @"Go amonxu.com";
    if (![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]) {
    [text writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];
    } else {
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingURL:fileURL error:nil];
    [fileHandle seekToEndOfFile];
    [fileHandle writeData:[text dataUsingEncoding:NSUTF8StringEncoding]];
    [fileHandle closeFile];
    }


    // read data
    NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.domain.groupName"];
    NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"fileName"];
    NSString *fileContent = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:nil];

cover
众所周知,iOS 10 以后,苹果官方推出了Notification Service Extension,查看文档,UNNotificationServiceExtension的说明是,An object that modifies the content of a remote notification before it's delivered to the user.,也就是在一个远程通知展示给用户之前,可以通过UNNotificationServiceExtension来修改这个通知。

废话少说,直接开干吧。

一、如何监听收到推送

  1. 在原有的项目上新建一个 Target,如图,选择创建
    image-20190304210307444

  2. 在服务端推送的内容中新增一个字段mutable-content": "1",与alert/badge/sound处于同一层级。例如在推送平台中测试推送时,设置如下:image-20190304212029192

  3. 先运行项目的主Target,然后再运行Notification Service Extension,手动选择主Target

  4. 发送测试推送,可以看到执行进入 Target 项目中的- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler

二、如何统计

  1. 如果是接入第三方推送平台,可以查看是否支持。如 极光的 Notification Service Extension 相关接口
  2. 如果自己实现的话,有以下两种方案
    1. 在通知扩展项目中写网络请求,将推送到达数据发送到后端服务器
    2. 将推送到达数据通过App Group保存到 App 的本地,在主 Target 中再处理

三、如何使用 App Groud 实现数据共享

  1. https://developer.apple.com 登录,创建 App Group

  2. 在项目中配置,target - Capabilites - App groups

  3. 代码中使用

    1. NSUserDefaults 中使用

      1
      2
      3
      4
      5
      6
      NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.company.appGroupName"];
      // write data
      [userDefaults setValue:@"value" forKey:@"key"];

      //read data
      NSLog(@"%@", [userDefaults valueForKey:@"key"]);
  1. NSFileManager 中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // write data
    NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.domain.groupName"];
    NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"fileName"];

    NSString *text = @"Go amonxu.com";
    if (![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]) {
    [text writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];
    } else {
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingURL:fileURL error:nil];
    [fileHandle seekToEndOfFile];
    [fileHandle writeData:[text dataUsingEncoding:NSUTF8StringEncoding]];
    [fileHandle closeFile];
    }


    // read data
    NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.domain.groupName"];
    NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"fileName"];
    NSString *fileContent = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:nil];

早高峰的地铁里,
人群席卷了站台,
十秒后空无一人,
除了那只孤独的鸟。
它瘦削的躯干,
高挑的气质,
两腿交叉在那,
静静地立着,
仿佛不属于这个世界。


本是,
林中鸟,
溪中水,
山涧清风,
田中野兔,
你在这里,
带来谁的问候,
带走谁的孤独?

列车越渐快速的远行,
它的身影被拉的愈发修长,
它仍低头兀自欣赏自己,
列车没能带走了她,
它却带走了路人的心。

原文链接:https://my.oschina.net/feichexia/blog/196575

JVM 性能调优参考资料:

《Java虚拟机规范》

《Java Performance》

《Trouble Shooting Guide for JavaSE 6 with HotSpot VM》: http://www.oracle.com/technetwork/java/javase/tsg-vm-149989.pdf

《Effective Java》

VisualVM: http://docs.oracle.com/javase/7/docs/technotes/guides/visualvm/

jConsole: http://docs.oracle.com/javase/1.5.0/docs/guide/management/jconsole.html

Monitoring and Managing JavaSE 6 Applications: http://www.oracle.com/technetwork/articles/javase/monitoring-141801.html

BTrace:https://kenai.com/projects/btrace

英文原文:
https://engineering.linkedin.com/blog/2016/02/eliminating-large-jvm-gc-pauses-caused-by-background-io-traffic

译文原文:
https://www.jianshu.com/p/ce9b3f0a90f2

这里就只贴出结论总结吧:

有低延迟要求的Java应用程序需要极短的JVM GC停顿。但是,当磁盘IO压力很大时,JVM可能被阻塞一段较长的时间。

我们对该问题进行了调查,并且发现如下原因:

  1. JVM GC需要通过发起系统调用write(),来记录GC行为。
  2. write()调用可以被后台磁盘IO所阻塞。
  3. 记录GC日志属于JVM停顿的一部分,因此write()调用的时间也会被计算在JVM STW的停顿时间内。
    我们提出了一系列解决该问题的方案。重要的是,我们的发现可以帮助JVM实现来改进该问题。对于低延迟应用程序来说,最简单有效的措施是将GC日志文件放到单独的HDD或者高性能磁盘(例如SSD)上,来避免IO竞争。


🍺 show-busy-java-threads

用于快速排查JavaCPU性能问题(top us值过高),自动查出运行的Java进程中消耗CPU多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用。
目前只支持Linux。原因是MacWindowsps命令不支持列出进程的线程id,更多信息参见#33,欢迎提供解法。

PS,如何操作可以参见@bluedavy《分布式Java应用》的【5.1.1 CPU消耗分析】一节,说得很详细:

  1. top命令找出有问题Java进程及线程id
    1. 开启线程显示模式(top -H,或是打开top后按H
    2. CPU使用率排序(top缺省是按CPU使用降序,已经合要求;打开top后按P可以显式指定按CPU使用降序)
    3. 记下Java进程id及其CPU高的线程id
  2. 用进程id作为参数,jstack有问题的Java进程
  3. 手动转换线程id成十六进制(可以用printf %x 1234
  4. 查找十六进制的线程id(可以用vim的查找功能/0x1234,或是grep 0x1234 -A 20
  5. 查看对应的线程栈,以分析问题

查问题时,会要多次上面的操作以分析确定问题,这个过程太繁琐太慢了

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
show-busy-java-threads
# 从所有运行的Java进程中找出最消耗CPU的线程(缺省5个),打印出其线程栈

# 缺省会自动从所有的Java进程中找出最消耗CPU的线程,这样用更方便
# 当然你可以手动指定要分析的Java进程Id,以保证只会显示出那个你关心的那个Java进程的信息
show-busy-java-threads -p <指定的Java进程Id>

show-busy-java-threads -c <要显示的线程栈数>

show-busy-java-threads <重复执行的间隔秒数> [<重复执行的次数>]
# 多次执行;这2个参数的使用方式类似vmstat命令

show-busy-java-threads -a <运行输出的记录到的文件>
# 记录到文件以方便回溯查看

show-duplicate-java-classes -S <存储jstack输出文件的目录>
# 指定jstack输出文件的存储目录,方便记录以后续分析

##############################
# 注意:
##############################
# 如果Java进程的用户 与 执行脚本的当前用户 不同,则jstack不了这个Java进程
# 为了能切换到Java进程的用户,需要加sudo来执行,即可以解决:
sudo show-busy-java-threads

show-busy-java-threads -s <指定jstack命令的全路径>
# 对于sudo方式的运行,JAVA_HOME环境变量不能传递给root,
# 而root用户往往没有配置JAVA_HOME且不方便配置,
# 显式指定jstack命令的路径就反而显得更方便了

# -m选项:执行jstack命令时加上-m选项,显示上Native的栈帧,一般应用排查不需要使用
show-busy-java-threads -m
# -F选项:执行jstack命令时加上 -F 选项(如果直接jstack无响应时,用于强制jstack),一般情况不需要使用
show-busy-java-threads -F
# -l选项:执行jstack命令时加上 -l 选项,显示上更多相关锁的信息,一般情况不需要使用
# 注意:和 -m -F 选项一起使用时,可能会大大增加jstack操作的耗时
show-busy-java-threads -l

# 帮助信息
$ show-busy-java-threads -h
Usage: show-busy-java-threads [OPTION]... [delay [count]]
Find out the highest cpu consumed threads of java, and print the stack of these threads.

Example:
show-busy-java-threads # show busy java threads info
show-busy-java-threads 1 # update every 1 second, (stop by eg: CTRL+C)
show-busy-java-threads 3 10 # update every 3 seconds, update 10 times

Output control:
-p, --pid <java pid> find out the highest cpu consumed threads from the specified java process,
default from all java process.
-c, --count <num> set the thread count to show, default is 5.
-a, --append-file <file> specifies the file to append output as log.
-S, --store-dir <dir> specifies the directory for storing intermediate files, and keep files.
default store intermediate files at tmp dir, and auto remove after run.
use this option to keep files so as to review jstack/top/ps output later.
delay the delay between updates in seconds.
count the number of updates.
delay/count arguments imitates the style of vmstat command.

jstack control:
-s, --jstack-path <path> specifies the path of jstack command.
-F, --force set jstack to force a thread dump.
use when jstack <pid> does not respond (process is hung).
-m, --mix-native-frames set jstack to print both java and native frames (mixed mode).
-l, --lock-info set jstack with long listing. Prints additional information about locks.

cpu usage calculation control:
-d, --top-delay specifies the delay between top samples, default is 0.5 (second).
get thread cpu percentage during this delay interval.
more info see top -d option. eg: -d 1 (1 second).
-P, --use-ps use ps command to find busy thread(cpu usage) instead of top command,
default use top command, because cpu usage of ps command is expressed as
the percentage of time spent running during the entire lifetime of a process,
this is not ideal.

Miscellaneous:
-h, --help display this help and exit.

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ show-busy-java-threads
[1] Busy(57.0%) thread(23355/0x5b3b) stack of java process(23269) under user(admin):
"pool-1-thread-1" prio=10 tid=0x000000005b5c5000 nid=0x5b3b runnable [0x000000004062c000]
java.lang.Thread.State: RUNNABLE
at java.text.DateFormat.format(DateFormat.java:316)
at com.xxx.foo.services.common.DateFormatUtil.format(DateFormatUtil.java:41)
at com.xxx.foo.shared.monitor.schedule.AppMonitorDataAvgScheduler.run(AppMonitorDataAvgScheduler.java:127)
at com.xxx.foo.services.common.utils.AliTimer$2.run(AliTimer.java:128)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)

[2] Busy(26.1%) thread(24018/0x5dd2) stack of java process(23269) under user(admin):
"pool-1-thread-2" prio=10 tid=0x000000005a968800 nid=0x5dd2 runnable [0x00000000420e9000]
java.lang.Thread.State: RUNNABLE
at java.util.Arrays.copyOf(Arrays.java:2882)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:572)
at java.lang.StringBuffer.append(StringBuffer.java:320)
- locked <0x00000007908d0030> (a java.lang.StringBuffer)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:890)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:869)
at java.text.DateFormat.format(DateFormat.java:316)
at com.xxx.foo.services.common.DateFormatUtil.format(DateFormatUtil.java:41)
at com.xxx.foo.shared.monitor.schedule.AppMonitorDataAvgScheduler.run(AppMonitorDataAvgScheduler.java:126)
at com.xxx.foo.services.common.utils.AliTimer$2.run(AliTimer.java:128)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)

......

上面的线程栈可以看出,CPU消耗最高的2个线程都在执行java.text.DateFormat.format,业务代码对应的方法是shared.monitor.schedule.AppMonitorDataAvgScheduler.run。可以基本确定:

  • AppMonitorDataAvgScheduler.run调用DateFormat.format次数比较频繁。
  • DateFormat.format比较慢。(这个可以由DateFormat.format的实现确定。)

多执行几次show-busy-java-threads,如果上面情况高概率出现,则可以确定上面的判定。
因为调用越少代码执行越快,则出现在线程栈的概率就越低。
脚本有自动多次执行的功能,指定 重复执行的间隔秒数/重复执行的次数 参数。

分析shared.monitor.schedule.AppMonitorDataAvgScheduler.run实现逻辑和调用方式,以优化实现解决问题。

声明

转载自 https://github.com/oldratlee/useful-scripts
致谢 oldratlee