Amon's Blog

猛猛如玉

coverimg

前言

随着公司业务的爆炸式的增长,需求规模和用户规模也迅速地膨胀起来,这样给系统的三高(高性能、高并发、高可用)以及扩展性、可维护性都带来了考验。而旧系统因为早期设计的各种局限性(如早期参与人员的水平、架构设计的前瞻性、老板的急性子等等),逐渐满足不了现状和未来的新需求,暴露出各种问题。开发人员们像是拖着老破车上高速,苦不堪言。(说人话:老系统代码的坑太深了,开发们填不住了,要么被坑埋了,要么弃坑逃跑了…)

那么这个时候,通常要面临一个问题:是继续填坑还是跑路走人 选择重构。填坑是不可能的,这辈子都不可能的。而选择重构是需要壮士断腕的勇气,因为重构是一项老大难、一件耗时耗力的事情,且多少会对现有业务开发造成影响,甚至是停滞。因此大多时候得不到产品经理和老板的支持,他们关心的只有一个:下个需求什么时候能上!至于其他的,都是你们研发该操心的。

自己选择的重构路,跪着也要走完。如何来一次就干就干的重构呢?根据互联网常见项目重构流程,以及我的亲身参与的重构项目经历,梳理大中小型系统的常见重构流程如下:

零:说服业务方

重构不单是研发团队的事情,更是整个项目团队的事情。重构可以提升系统的三高,也可以优化改善业务流程,满足新的业务诉求等等。重构需要投入大量资源,必须要得到业务方的支持。通常这个时候需要对他们晓之以理,动之以情,阐述清楚重构的利弊,以及不重构的要害。在得到他们的支持后,重构的工作便正式开展。

参与人员:技术 Leader

一:树立重构目标,有的放矢

重构是一项工程,是一场持久战,它不是一两个迭代、甚至一两个月能做好的事情,需要投入大量的人力、物力、时间精力等。那么在这场旷日持久的战斗中,我们的目标是什么?是通过更优秀更合理的架构来满足系统三高的需求,还是想通过重构来提高代码质量,或者引入新的技术和框架来升级整个系统,抑或通过重构来优化业务流程,实现原来实现不了的需求。有了目标后,才能做到有的放矢。

参与人员:技术 Leader,架构师

二:确定重构的范围,并对重构作出预测

重构通常有以下几个级别的重构

  • 平台级别重构。针对整体平台的重构,如阿里早期是 LAMP 架构,后来整体迁移到了 Java 平台。
  • 系统级别重构。针对业务系统的重构,如通过引入微服务架构或者 SOA 架构,分解单体应用。
  • 架构级别重构。如通过架构的调整和重新设计,改善原有架构的不合理之处。如通过分层使业务解耦,引入缓存设计提升系统高并发等。
  • 业务级别重构。常见为某些业务需求因为系统设计的不合理性导致无法满足或有缺陷满足,需要通过业务系统的重构调整或数据库的重构来解决。
  • 模块/代码级别重构。这是最常见的重构。通常指使用设计模式、封装继承、优化拆解代码,使得代码的结构更良好,运行效率更高。

确定这次重构是属于什么级别,确定重构的整体范围的大小,确定重构的技术选型,进而对重构工作进行科学的评测和预估。比如需要投入哪些成本,需要投入的人力和时间是多少,在重构的过程中能否支撑正常业务需求等等。在有了这些预测后,也对业务方有个交代,尤其是当他们追在后面问什么时候能上新需求。

参与人员:技术 Leader,架构师,研发人员

三:旧系统的熟悉和业务梳理

重构不是和旧系统说散就散,而是要不断和旧系统战斗的过程。知己知彼,百战不殆。重构不仅需要清楚新系统的目标和未来,更需要对旧系统非常熟悉(尤其是坑)。此时需要参与重构的人员(尤其是参与旧系统的人员)来对旧系统业务和系统进行梳理,对原有资料信息进行收益和整理的工作,对旧系统的关键代码和数据库设计进行 Review等等。

以下是重构旧系统前需要准备的常见工作:

  • 旧系统资料和信息的收集,包含且不限于系统相关的设计文档和技术文档等文档资料,架构图、UML 图,数据库设计 ER 图等图形化资料
  • 业务线和业务流程的梳理,整理业务线上的各大项目、业务流程,并输出为文档
  • 旧系统关键代码的 Review

有相关疑难点及时与相关与业务线上的人员沟通,将问题解决在”襁褓”中。

参与人员:技术 Leader,架构师,研发人员

四:数据库重构

如果在重构中需要涉及数据库的重构,数据库的重构一般是最先开始的一步。系统需要重构的直接原因,也大多和数据库有关。在数据库重构时,我们清楚旧系统中数据库的各种设计缺陷和使用障碍,那么就可以对症下药,如通过三大范式或反范式来设计表,是否需要分库分表等等。

参与人员:DBA,架构师

五:后台系统重构

后台系统重构前,必须需要依照前文所述的一些设计和技术文档。这些文档输出后并经讨论成型后,架构师进行系统架构设计,后台开发人员进行具体编码工作。通常这个过程是耗时最长的,也是非常重要的一环。后台的架构设计水平,决定着系统重构的水平,业务代码的质量,决定着系统重构的质量。

因为这个过程比较漫长,且成果无法立竿见影。所以通常采用敏捷开发的模式,通过迭代的方式来进行后台系统重构。迭代的方式有几个好处:

  1. 需要将整个重构过程进行有效规划和量化,做到胸有成竹
  2. 每个阶段能有可见的成果,确保团队在长时间的重构过程中不陷于泥潭
  3. 对已重构好的部分可以及时进行联调测试或观察,不断在迭代中总结、在总结中迭代

另外在后台系统重构时,也需要有明确量化的目标和标准,比如各系统和业务模块支持多少 QPS,接口响应时间多长时间等,这样团队才能在重构的过程中不至于为了重构而重构。

在重构过程中,定期进行 Code Review,及时发现重构的问题和质量的问题,避免出现破窗效应,引入拙劣的设计或垃圾代码,进而破坏整个系统。

参与人员:技术 Leader,架构师,研发人员

六:数据迁移与检查

如果涉及数据库重构时,在新的数据库设计好后,就会有面临数据迁移的问题。一般分为全量迁移和增量迁移,全量迁移是将旧系统的数据一次性迁移到新的数据库中,增量迁移是在实行全量迁移后旧系统新产生的数据迁移到新系统上来,增量迁移一直到旧系统下线不再产生新数据后。通常迁移都是通过编写脚本或程序来实现,拒绝人工操作。

迁移后自然需要对比新旧系统的数据,同样可以通过脚本或程序来进行对比,查缺补漏,定位分析。

参与人员:DBA,研发人员

七:系统检查、联调与测试

在后台系统重构到一定程度时,同样也需要编写脚本和程序来对新旧系统的业务接口进行检查,及时发现重构中的问题,必要时候进行架构调整和数据库调整。当然,在重构时,开发人员能提高单元测试覆盖率当然是更好不过。当各系统和模块的依赖解决的差不多时,可以开始联调工作。

当然最后还需要系统性的测试,如功能性测试、稳定性测试、性能测试,本地测试、模拟线上环境测试等。测试中发现的问题经验证修复后,达到上线的标准,即可灰度上线。

参与人员:架构师,研发人员,测试人员

八:灰度发布与观察

万里长征已经走到最后,也到了最紧要的关头。灰度发布时,只接入一小部分流量,并及时跟踪和分析线上的 log 与监控告警,一有问题及时解决。当新系统趋于稳定时,可以逐渐加大灰度发布的范围和接入的流量,同时继续跟踪线上 log 与监控告警。

参与人员:运维人员,测试人员,研发人员

九:系统切换

在系统切换时,需要提前制订系统切换方案,包含相应的规划与流程,甚至是应急预案与回滚方案,避免走一步看一步。

参与人员:运维人员,测试人员

结语

通过上述几个步骤后,我们成功对系统进行重构。

重构是一项大工程,但经历重构后的系统也并非完美无缺。重构不是终点,更像是起点。

2019 年 3 月 17 日,经过周五和周六的两天忙碌后,周日准备先放松一下。在拒掉了朋友一起遛娃的邀约后,带着老婆女儿来到位于西乡的基督教宝安堂。

在和气的保安大叔指引下,我们来到二楼的主堂。此时11点多,距离下午的主日崇拜还有几个小时,堂内只有零零散散几个人,有相貌普通且发型有些凌乱的年轻女人,也有怀抱着婴儿的少妇和衣着光鲜的老妇人。整个屋子是一排排的暗红色长椅,每一把长椅的后面摆放了几本一样深色的书,一本圣经,一本赞美诗,另有一小册赞美诗短篇。这些专用来给信徒们直接使用,而不需要自带经文和赞美诗。

在长椅上坐了一会后,我往大堂的最前方讲台走去。遥看着讲台上下摆放着几排绿植。米黄色的背景墙上一个深色的十字架,上面“以马内利”四个大字。我注视着十字架一步步走过去,内心感到无比平静。走到讲台前方,看到十字架下面有一方台子,上面两行字,上一行“A Ω”,下一行“为的是纪念主”。

在主堂呆了一会后,我们三人沿着楼梯去三楼看看。走在楼梯间仿佛听到奏乐声,走进三楼的副堂发现是几位年轻人在摆弄乐器,有钢琴,有吉他,他们不紧不缓地弹着《江南》。这是我最爱的歌曲,但伴奏音乐倒是第一次听,听来煞是有感觉。我们就这样听着音乐,往大堂后方走去,但见堂内有穿着校服充电看电视的学生,有双手紧握、闭着眼睛激情祷告的一对母女,还有说着笑着弹着音乐的年轻人。

走到后方的窗台边上,我转头一看,但见墙上画着一副巨大的油画,耶稣身披白衣手执长杖在牧羊,一只只白色的绵羊在兀自地吃草。放眼望去,外面阳光灿烂无比,屋内也亮亮堂堂。

这一刻,沐浴在主的光辉下,如沐春风。

后记,回去后,查了一下“A Ω”的意思,A 是 Alpha,起始之意,Ω 是 Omega,终了的意思。“A Ω”就是从始至终。

jstat 用于监视 Java 虚拟机(JVM)的统计数据。这个命令是实验性的,不受支持。

简介

jstat <一般选项 | 输出选项> <vmid> <时间间隔> <count>

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

一般选项

​ 一个通用命令行选项,-help 或者 -options。详见 一般选项

输出选项

​ 包含单个 statOption的一个或多个输出选项,以及 -t,-h 和 -J 选项。详见输出选项

vmid

​ 虚拟机标识符,是指示目标 JVM 的一个字符串。一般格式如下:

[protocol:][//]lvmid[@hostname[:port]/servername]

vmid 的语法对应 URI 的语法。vmid 字符串可以是从表示本地 JVM 的一个整数,到指定通信协议、端口和其他特定实现值的更复杂的结构。详见虚拟机标识符

间隔时间

​ 以指定单位秒(s)或毫秒(ms)的采样间隔。默认是秒,必须为正整数。当指定时,jstat 命令会在每个间隔时间进行输出。

数量

​ 要显示的样本数。默认无穷大,这样 jstat 命令会一直输出的统计信息,直到 JVM 终止或者 jstat 命令终止。必须为正整数。

描述

jstat 命令显示被监测的 Java HotSpot VM的性能统计数据。用虚拟机标识符或者vmid选项标识目标 JVM。

虚拟机标识

vmid 的语法对应于 URI 的语法:

[protocol:][//]lvmid[@hostname[:port]/servername]

protocol

​ 通信协议,如果省略协议且没有指定主机名,默认协议是特定于平台的优化本地协议。如果省略了协议并指定主机名,那么默认协议是 RMI

lvmid

​ 目标 JVM 的本地虚拟机标识。lvmid 是一个特定于平台的值,它唯一地标识系统上的 JVM。lvmid 是虚拟机标识符唯一需要的组件。lvmid 通常是(但不一定是)目标 JVM 进程的操作系统的标识符。你可以使用 jps 命令来确定 lvmid,此外也可以在 Solaris、Linux、OS X平台上使用 ps命令来确定 lvmid,在 Windows上可以使用 Windows 任务管理器来操作。

hostname

​ 目标主机的主机名或 IP 地址。如果省略了 hostname,默认是本地主机。

port

​ 与远程服务器通信的端口。如果省略主机名或协议指指定优化的本地协议,则忽略端口值。否则,端口参数的处理是特定于实现的。对于默认的 rmi 协议,端口值指示远程主机上 rmiregistry 的端口号。如果省略端口值且协议值指示 rmi,则使用默认 rmiregistry 的端口。

servername

​ servername 参数的矗立取决于实现。对于优化的本地协议,这个字段被忽略。对于 rmi 协议,它表示远程主机上 RMI 远程对象的名称。

选项

jstat 命令支持两种选项类型:一般选项和输出选项。一般选项用来显示简单的用法和版本信息。输出选项决定统计输出的内容和格式。

所有选项及其功能都可能在未来的版本中更改或移除。

一般选项

如果你指定了一个一般选项,则不能再指定任何其他选项或参数。

-help

​ 显示帮助信息

-options

​ 显示一个统计选项列表,详见输出选项

photo-1523240795612-9a054b0db644


三月份的深圳,春寒料峭。

天气阴阴晴晴,反反复复,犹犹豫豫的徘徊在春天和冬天之间。此时,各行各业普遍不景气,到处弥漫着互联网寒冬和裁员的凋零气息。但没什么比二哥心更冷的了。

四年前的广州,热火朝天。

二哥从二线互联网公司出来,彼时,以社交和 O2O 为首的移动互联网乱战纷飞的局面逐渐消停,微信、支付宝、今日头条等移动互联网巨头已经浮出水面。AI 人工智能,则如同一轮红日,冉冉升起。

二哥一头扎入 AI 的大潮中,在一个创业公司做开发。二哥的猛子扎的之深,以至于未来几年都不能上岸。创业嘛,理想嘛,总是要有点牺牲的,二哥抛弃了呆了几年的广州和优渥的待遇,孤身来到深圳,和降薪的条件。

都说创业公司拥抱变化,二哥怎么也没想到变化来的太快了点,别怕,公司还在,只是技术团队几乎全部出走。原来后台留下的一堆坑,一时半会也找不到合适的人来填,二哥赶鸭子上架般的顶了上去。谁曾想这一顶,就是四年。

四年后,热血青年的二哥感觉垂垂老矣;四年后,秀发茂密的二哥感觉额头发凉;四年后,一人吃饱全家不饿的二哥已经买了车房结婚。二哥感觉自己需要走了,再不走就老了。尽管在公司已经是老人了,但也差不多快成那个老人了。

二哥曾想过随着公司的发展壮大,随着业务的扩张开拓,总有一天自己是有机会回去广州的。或者至少,很体面地回去。没想到这一天来的也太快了点,只不过,是用了另一种方式。

都说人们当失去父母的时候,会真正地面临着死亡。研发的老大离开公司以后,二哥变成了研发这边的老大。老大在的时候,老大为研发的弟兄们遮风挡雨,PK 掉各种 Boss 需求,拒绝掉各种领导的拍脑袋 idea,争取来虽然不多至少也算有的蝇头福利。老大走后,压力也来到了二哥的身上,二哥也暗暗地计算起来:房贷首付的钱,还有几个月可以还完,买车借的钱,什么时候可以周转过来…一边又继续承受着来自各方的压力。

针对二哥的卸磨杀驴第一刀砍下来了,二哥要被调往其他部门,二哥拒绝了,但二哥最后还是走了。伴随着不甘、解脱、喜悦、苦涩、希望、和绝望等等,二哥离开了。

二哥 last day 的那天中午,我们一起出去吃了顿快餐。想起以前公司包两餐时,二哥总是吃的很清淡,一是养生,二是有了老婆要养身(bei yun)。我知道这大概是当年高峰期疯狂加班落下的毛病。午饭二哥吃了鱼,然后被鱼刺卡到。我心想,这些过去的所有的事,依然是你如鲠在喉。

二哥离开公司的时候,我和另外几个同事送至电梯前,我们给二哥拍了张照,二哥给我们录了视频。视频里,我朝二哥拘谨地挥了挥手。就像四年前,二哥面试我时,我向他先打了招呼:哎,你好!

后记:

二哥和我共事了三年半的时间,三年半的时间里,我们一起经历公司的初创到成长,进步和退步,危机和机遇。偏偏创业小公司又是风雨交加的地方,三年半就好像过了十年。中间发生了很多,我的绝不止想写这么多。但是,二哥,都懂吧,我不写了。

new year, new adventures 2019

2019 年二月过去了。

二月是静悄悄的,这一个月中,它大多时候是被农历代替的,我们用腊月二十七代表2月1号,大年三十代替2月4号,元宵节代替2月19号,也就2月14号这一天,它不被称作正月初十,而是叫情人节。

二月是容易被遗忘的,在此之前的一月,人们混淆于到底是 2019 年还是 2018 年,是该迎接这新年,还是该珍惜这旧年。就这样到了2月,人们发现对新年并没有了什么期待,反倒和从前的那些年月并无什么两样。

二月是短暂的,它开始于春节长假伊始,长假结束时它已然进入半晌,等到调整完工作节奏,二月已入尾声。加上天生地比其他月份少去十分之一,没等你痛惜二月地匆匆而逝时,它就结束使命了。

二月是忙碌的,见一年见不到的人,吃一年吃不到的饭,喝一年喝不到的酒。从折腾着挤上回家的车,到再折腾着挤上回来的车,没有一时的歇息。

饱含对生活的热爱吧!饱含对未来的渴望吧!唯有把握现在,才能拥有未来,才能缅怀过去。

2019, Make different!

pic

旧梦

雨后,空气中满是清冷,到时都是湿漉漉的,走在一条泥泞的小路上。沾水的草地,暗黄的天空,钻入衣服的空气。

初中同学 45 度俯视着我,我讨厌别人这样压迫着看我,也讨厌他那婆婆妈妈般的性格,哪怕是十一年没见了。

逃离和他的相遇后,我穿过蜿蜒的小路,来到一个荒废的操场,半人高的野草,绿的发亮。破旧不堪的校门半掩在草丛里,被日晒雨淋显得灰白的油漆已经脱落了一大半,露出丑陋的铁锈色,一把生满红褐色铁锈的大锁肆无忌惮的挂在门上,嘲笑着我这个陌生的访客。顶部几根枪头直插云天,愤怒的指向阴暗的天际。

这番落败的景象实在让我觉得无味,我只好继续前行,忽然进入一个房子里。像是被犁翻耕过的土地,像是爆炸后产生的废墟,地上密密麻麻铺满了室内原来物品的碎片,均匀到像是秋天树林里铺满落叶的地面。

我拾起一个碎片,一张照片,是关于我的。我倏然悲从中来,这张照片上面是十几年前的我,而今所有记忆皆毁于一旦、尽为碎片。转身看屋内,都是我的记忆,小学初中、高中大学、暑假、寒假、过年…一张张,一片片,拼接成我的前二十几年。

我应该是觉得我老了,我应该是觉得我应该痛哭流涕,脑中环绕着:世间最大的痛苦是发现一切都旧了。发现旧时的自己,发现旧时的事物,全化为灰烬,剩下一个旧的自己,残留在世上。

想至此,我甚为怀念那个小男孩,他贪玩又聪明,他调皮的事迹和他优秀的成绩一样在方圆几里的村子里人尽皆知。他身体结实、力气惊人,健康又快乐地成长着,除了…略微缺乏的父爱。
head

假如他看到眼前这个旧的,废物一般的自己,他一定很不屑。

你这么矮这么瘦这么小吗?
你的名牌大学呢?
你不是会有钱到了不起吗?
你不是说不会忘记你的兄弟朋友吗?
切,瞧你这熊样,我能轻易打倒你。

眼前突然一黑一亮,嚯!我醒了。顿时我悲从心来,打开窗帘,夜色放浅,天空渐明。才知我做了一个旧梦,梦里一切都变旧了。我想和谁说,我做了一个旧梦,梦里一切都变旧了。但想无人与说,且当聊作孤梦。


后记

原文作于2016年11月6日清晨,2年后的今天被发现于备忘录。
重读的感觉,
先是思乡,思念那种雨后乡下的感觉。
接着是恐怖,恐怖的是有人看着我,且我忘记了他是谁。
然后是压抑,至今还能感受到梦中那种压抑。
后面就是怀念,怀念那亦真亦假的过去和记忆。
最后是悲哀,痛苦,直到长叹一声:唉!

有关照片碎一地的记忆我一直在记忆中仿佛昨天,直到后面听到许飞的《父亲写的散文诗》,方知此种感觉正是 「回忆」 二字。

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](/Users/amon/Library/Application Support/typora-user-images/E9FDE38F-8D19-4587-8308-6629648903FE/Pasted%20Graphic%201.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"]);
    2. 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];
0%