碎片知识

DevOps

可参考码农翻身《什么是DevOps》
目前对 DevOps 有太多的说法和定义,不过它们都有一个共同的思想:“解决开发者与运维者之间曾经不可逾越的鸿沟,增强开发者与运维者之间的沟通和交流”。而我个人认为,DevOps 可以用一个公式表达:

文化观念的改变 + 自动化工具 = 不断适应快速变化的市场 其核心价值在于以下两点:

  • 更快速地交付, 响应市场的变化。
  • 更多地关注业务的改进与提升。

IT技术架构也随着系统的复杂化而不断地变化革新,从早期所有服务的All In One发展到现在的微服务架构、从纯手动操作到全自动化流程、从单台物理机到云平台,下图展示了IT技术革新的变化:
其实DevOps核心思想就是:“快速交付价值,灵活响应变化”。其基本原则如下:

  • 高效的协作和沟通
  • 自动化流程和工具
  • 快速敏捷的开发
  • 持续交付和部署
  • 不断学习和创新

个人摘录码农翻身文章的理解:

  • 研发团队要微服务改造,为频繁部署做准备,也方便快速回滚。
  • 研发和运维不能互相推卸责任,互相指责,而要共同担责了。一个项目的开发、部署、维护,是研发和运维双方的事情,双方都要对业务负责,出了什么问题,要通力合作,共同解决。
  • 运维人员也要了解业务,Code变化带来的影响要告知运维人员
  • 开发人员工作的开发/测试环境要尽可能地和生产环境一致
  • 部署工作要完全自动化起来,部署的脚本由运维部门主导,有问题研发部门来辅助,将来这个部署脚本要在所有的环境都用起来!
  • 要有度量指标去衡量DevOps,例如失败部署的百分比,用户开的ticket数目,故障恢复的平均时间等等
  • 无论用了什么工具、方法,如果最后没有减少需求从提出到上线,交付给用户使用的时间,那都是失败的,想尽办法去减少这个时间,不是一次、两次,而是持续、稳定地交付给用户。

何为分布式系统

维基百科定义:

分布式系统是一种其组件位于不同的联网计算机上的系统,然后通过互相传递消息来进行通信和协调。为了达到共同的目标,这些组件会相互作用

  • 服务化的确是分布式系统,但分布式系统不仅仅停留在那些服务化的模式(SOA、ESB、微服务...)上。

    服务化的本质是“分治”,而“分治”的前提是先要拆,然后才谈得上如何治。这时,高内聚、低耦合的思想在拆分过程中起到了一个非常重要的作用

  • 分布式系统中会运用中间件(MQ、RPC、DAL...),但分布式系统却不仅仅停留在用了什么中间件上

    中间件起到的是标准化的作用。中间件只是承载这些标准化想法的介质、工具,可以起到引导和约束的效果,以此起到大大降低系统复杂度和协作成本的作用。

看待一个“分布式系统”的时候,内在胜于表象。其实,只要涉及多个进程协作才能提供一个完整功能的系统,就是“分布式系统”

分布式系统本质

转自 InfoQ 张帆:《分布式系统的本质其实就是这两个问题》

分 治

分治,字面意思是“分而治之”,和我们的大脑在解决问题时的思考方式是一样的。我们可以将整个过程分为 3 步:分解 -> 治理 -> 归并。而分治思想的表现形式多样,分层、分块都是它的体现。
这么做的好处是:问题越小越容易被解决,并且,只要解决了所有子问题,父问题就都可以被解决了。但是,这么做的时候,需要满足一个最重要的条件:不同分支上的子问题,不能相互依赖,需要各自独立。因为一旦包含了依赖关系,子问题和父问题之间就失去了可以被“归并”的意义。在软件开发领域,我们把这个概念称为“耦合度”“内聚度”,这两个度量概念非常重要。

  • 耦合度:指的是软件模块之间相互依赖的程度。比如,每次调用方法 A 之后都需要同步调用方法 B,那么此时方法 A 和 B 间的耦合度是高的。
  • 内聚度:指的是模块内的元素具有的共同点的相似程度。比如,一个类中的多个方法有很多的共同之处,都是做支付相关的处理,那么这个类的内聚度是高的。

内聚度通常与耦合度形成对比。低耦合通常与高内聚相关,反之亦然。所以,当你打算进行分治的时候,耦合度和内聚度就是需要考虑的重点。

冗余

这里的冗余并不等同于代码的冗余、无意义的重复劳动,而是我们有意去做的、人为增加的重复部分。其目的是容许在一定范围内出现故障,而系统不受影响。
但是,单纯地为了备用而做的冗余,最大的弊端是,如果没有出现故障,那么冗余的这部分资源就白白浪费了,不能发挥任何作用。所以,我们才提出了诸如双主多活、读写分离之类的概念,以提高资源利用率。
不过也很显然,当故障影响范围大于你冗余的容量时,系统依然会挂。所以,既然你无法预知故障的发生情况,那么做冗余的时候需要平衡的另一端就是成本。相比更多的冗余,追求更好的性价比更合理一些。

再连接

分治和冗余讲究的都是分散化,最终形成一个完整的系统还需要将它们“连接”起来。天下没有免费的午餐,获得分布式系统价值的同时,这个“再连接”的过程就是我们相比集中式系统要做的额外工作。
如何将拆分后的各个节点再次连接起来,从模式上来说,主要是去中心化与中心化之分。
前者完全消除了中心节点故障带来的全盘出错的风险,却带来了更高的节点间协作成本。后者通过中心节点的集中式管理大大降低了协作成本,但是一旦中心节点故障则全盘出错。
另外,从技术角度来说,如何选择通信协议和序列化机制,也是非常重要的。
虽然很多通讯协议和序列化机制完全可以承担任何场景的连接责任,但是不同的协议和序列化机制在适合的场景下才能发挥它最大的优势。比如,需要更高性能的场景运用 TCP 协议优于 HTTP 协议;需要更高吞吐量的场景运用 UDP 协议优于 TCP 协议,等等。

事物最本质的东西是恒定的、不变的,可以指引我们的工作方向。分布式系统的本质也是这样。
例如,这样的“分治”方案耦合度和内聚度是否最优,这样做“冗余”带来的收益是否成本能够接受。只要持续带着这些思考,我们就好像拿着一杆秤,基于它,我们就可以去衡量各种变量影响,然后作权衡。比如成本、时间、人员、性能、易维护等等。也可以基于它去判断什么样的框架、组件、协议更适合当前的环境。
需要不断的权衡,也意味着分布式系统的设计工作一定不是一步到位,而是循序渐进的。因为过分为未知的未来做更多的考量,最终可能都会打水漂。所以,建议以多考虑 1~2 步为宜。假如以你所在的团队中对重大技术升级的频率来作为参考的话,做可供 2 个升级周期的设计,花一个升级周期的时间先实现第一阶段,下个阶段可以选择直接实现剩下部分,也可继续进行 2 个升级周期设计,开启一个循环,持续迭代,并且不断修正方向以更贴近现实的发展,就如下图这样。

分布式系统中最容易被忽视的坑

1.网络并不是可靠的

需要在系统设计中,尽可能地考虑到:当前节点所依赖的其他节点由于各种原因无法与之正常通信时,该如何保证其依然能够提供部分或者完整的服务。这个概念在软件领域被定义为“鲁棒性”

2.不同节点之间的通信是存在延迟的

我们不能以调用本地方法一样的认知去理解远程调用(RPC)。在目前的技术环境下,CPU 的运算能力长期保持高速增长,因此两个节点间通讯的大部分场景中,延迟的耗时总是大于目标节点进行逻辑运算的耗时。
由此可得,并不是将一个集中式系统拆分得越散,系统就越快。当中存在的延迟,不容忽视。

我们还需要慎重考虑是否有必要进行“同步”的 RPC 调用,以及尽量的降低调用次数等。这就是为什么我们说,循环调用不如一次批量调用,反复调用不如调用一次做进程内缓存 的道理。

3.带宽是有上限的

  • 实际环境中的传输速率大小,是由服务商所提供的带宽大小,以及网卡、网卡、交换机、路由器所支持的传输速率中的最小值决定的。
  • 内网与外网传输速率是不同的,一般都是内网大于外网。因为服务商所提供的带宽成本更高。
  • 同一个局域网内的节点是公用外网出入口的,所以尽可能的缩小在外网传输的数据,以降低占用“独木桥”外网的空间。

4.分布式并不直接意味着是“敏捷”了

拆分后需要做的额外工作如果没做好,可能会导致不是更快,而是更慢。最典型的现象如:

  • 发布更麻烦了
  • 排查问题更难了

关于这点,你需要秉持着工欲善其事必先利其器的思想。将建设协作相关的辅助性工作与分布式系统同时进行。比如:监控告警系统、配置中心、服务发现,以及批量部署、持续集成,甚至 DevOps 等。

5.数据由一份冗余成多份后如何保持一致

这个概念在软件领域被定义为「数据一致性」。

6.整个系统的不同部分可能是异构的

形容一个包含或者组成“异构网络”的产品,所谓的“异构网络”通常指不同厂家的产品所组成的网络。

在系统初期,同类技术下的差异往往不太被关注。但是随着分布式系统规模的逐渐扩大,在进入到成熟期的过程中,往往不可避免的会使系统成为异构的,有主动的,也有被动的。
关于这点,你要思考如何通过专制的方式进行标准化,来屏蔽这些差异带来的复杂度影响,使得有更多的精力投入到有价值的地方去。专制的特点是“约束”效果,标准化通过专制来进行可以降低许多为了方便而妥协问题。比如每个节点都通过统一的远程调用(RPC)中间件来做“连接”,就将如何连接异构节点的问题交由这个中间件来全权负责,而不是不同的团队各自实现一套。

Redis 添加分布式锁

incr、incrBy、setnx的加锁方式都是有缺陷的,还可以使用 set 方法加锁:

// "NX" 表示如果Redis中不存在key时,就设置key,"EX" 表示设置超时,expireSeconds 表示超时的值
jedisCluster.set(key, value, "NX", "EX", expireSeconds);  // SET IF NOT EXIST

// jedis 源代码
@Override
public String set(final String key, final String value, final String nxxx, final String expx,  
  final long time) {
return new JedisClusterCommand<String>(connectionHandler, maxRedirections) {  
  @Override
  public String execute(Jedis connection) {
    return connection.set(key, value, nxxx, expx, time);
  }
}.run(key);
}

而且还是原子的。
可参考https://blog.csdn.net/Dennis_ukagaka/article/details/78072274,其实jedis的每个可能会新增的操作都应该有这么一个与时间相关的原子性方法,不然还要我们自己写lua脚本。

另外有大神【芋道源码】文章:一文看透 Redis 分布式锁进化史(解读 + 缺陷分析)

Redis 踩坑

Redis 开发规范参考

  • AOF
    Redis 的AOF机制有点类似于Mysql binlog,是Redis的提供的一种持久化方式(另一种是RDB),它会将所有的写命令按照一定频率(no, always, every seconds)写入到日志文件中,当Redis停机重启后恢复数据库。
  • AOF重写
    1.随着AOF文件越来越大,里面会有大部分是重复命令或者可以合并的命令(100次incr = set key 100)
    2.重写的好处:减少AOF日志尺寸,减少内存占用,加快数据库恢复时间。
  • 单机多实例可能存在SwapOOM的隐患
    由于Redis的单线程模型,理论上每个redis实例只会用到一个CPU, 也就是说可以在一台多核的服务器上部署多个实例(实际就是这么做的)。但是Redis的AOF重写是通过fork出一个Redis进程来实现的,所以有经验的Redis开发和运维人员会告诉你,在一台服务器上要预留一半的内存(防止出现AOF重写集中发生,出现swap和OOM)。
  • meta信息
    作为一个redis云系统,需要记录各个维度的数据,比如:业务组、机器、实例、应用、负责人多个维度的数据,相信每个Redis的运维人员都应该有这样的持久化数据(例如Mysql),一般来说还有一些运维界面,为自动化和运维提供依据

双重校验锁实现单例

/**
 * 双重校验锁实现单例
 */
public class Singleton {  
    private static volatile Singleton instance;

    // 私有构造方法
    private Singleton() {}

    // 双重校验锁
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

web.xml 中 Filter 执行顺序

1)先执行带有url-pattern标签的filter,再执行带有servlet-name标签的filter。

<filter-mapping>  
    <filter-name>AppRequest1</filter-name>
    <url-pattern>*.do</url-pattern>
</filter-mapping>  
<filter-mapping>  
    <filter-name>AppRequest2</filter-name>
    <servlet-name>SomServlet</servlet-name>
</filter-mapping>  

2)如果同为url-patternservlet-name,则会按照在web.xml中的声明顺序执行。

https

CA 无法对整个报文进行加密,因为非对称加密的内容长度不能比公钥长,因此才生成一个摘要,对摘要进行加密。一个故事讲完https 黑客攻防日记

JDK 版本号区别

官方对于奇数版本与偶数版本区别的解释: 从JDK版本7u71以后,JAVA将会在同一时间发布两个版本的JDK。

  • 奇数版本:为BUG修正并全部通过检验的版本,官方强烈推荐使用这个版本。
  • 偶数版本:包含了奇数版本所有的内容,以及未被验证的BUG修复,Oracle官方表示:除非你深受BUG困扰,否则不推荐您使用这个版本。

【官方说明链接】

ConcurrentHashMap

HashMap不同的是,ConcurrentHashMap并不允许key或者value为null。HashTable虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的。ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也将这个判断留给用户自己决定是否使用ConcurrentHashMap。
可参考:ConcurrentHashMap能完全替代HashTable吗?

在 JDK8 中:ConcurrentHashMap 进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想(JDK7与JDK8中HashMap的实现),但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
可参考:ConcurrentHashMap总结

HashMap

JDK7中HashMap采用的是位桶+链表的方式,即我们常说的散列链表的方式,而JDK8中采用的是位桶+链表/红黑树(有关红黑树请查看红黑树)的方式,也是非线程安全的。当某个位桶的链表的长度达到某个阀值的时候,这个链表就将转换成红黑树。
可参考:JDK7与JDK8中HashMap的实现

多线程

进程是拥有资源的基本单位, 线程是CPU调度的基本单位

线程池

package com.hengyi.test;

import java.util.concurrent.ArrayBlockingQueue;  
import java.util.concurrent.Callable;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  
import java.util.concurrent.ThreadPoolExecutor;  
import java.util.concurrent.TimeUnit;

public class ExecutorsPoolController {

    public static void main(String[] args) {
        /**
         * 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
         * 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线
         * 程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制
         * ,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
         */
        ExecutorService service = Executors.newCachedThreadPool();
        /**
         * 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线
         * 程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程
         * 因为执行异常而结束,那么线程池会补充一个新线程。
         */
        ExecutorService service1  = Executors.newFixedThreadPool(100);
        /**
         * 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单
         * 线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新
         * 的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
         */
        ExecutorService service2 = Executors.newSingleThreadExecutor();

        /**
         * 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
         */
        ExecutorService service3 = Executors.newScheduledThreadPool(100);

        /**
         * ThreadPoolExecutor是JDK并发包提供的一个线程池服务,基于
         * ThreadPoolExecutor可以很容易将一个Runnable接口的任务
         * 放入线程池中。
         * 
         * 1. 参数解释
         * corePoolSize:         核心线程数,会一直存活,即使没有任务,线程池也会维护线程的最少数量
         * maximumPoolSize: 线程池维护线程的最大数量
         * keepAliveTime:      线程池维护线程所允许的空闲时间,当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。
         * unit: 线程池维护线程所允许的空闲时间的单位、可选参数值为:TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。
         * workQueue: 线程池所使用的缓冲队列,常用的是:java.util.concurrent.ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
         * handler: 线程池中的数量大于maximumPoolSize,对拒绝任务的处理策略,默认值ThreadPoolExecutor.AbortPolicy()。
         */
        ExecutorService service4 = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),100, 120L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(1000));

        Future<String> future = service4.submit(new Callable<String>() {

            @Override
            public String call() throws Exception {
                // TODO Auto-generated method stub
                return "我是线程,我是执行结果";
            }

        });

        try {
            System.out.println(future.get());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ExecutionException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        service3.execute(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("我是线程");
            }
        });
        /**
         *  submit execute
         *  submit会返回线程的执行结果
         */

    }

}

先删数据库再删缓存

在论坛上看到好多人说先删除缓存在更新数据库,这种逻辑是错误的,

第一种情况 先删缓存在删数据库:在多线程环境下,当一个线程把缓存删掉之后,另一个线程都缓存,都不到缓存就会直接读库,读到数据后就会更新缓存,先前的线程呢,才更新数据库,会造成缓存脏读的情况,很容易产生缓存脏读。

第二种情况 先删数据库再删缓存:在多线程情况下,当一个线程删除数据库,另一个线程读取缓存数据,读到的是缓存的数据,当先前一个线程删完数据库后就会更新缓存,这是缓存就正常了,产生了一次脏读。

分析对比:
1.第一种产生了长时间的脏读,第二种只有很短时间的脏读
2.第一种删除缓存后会造成缓存击穿,如果大量线程访问就会中造成数据库压力过大,第二种其他线程会读取缓存数据,不会对数据库造成很大压力,更新数据库后缓存马上就更新了。

尾递归

当递归调用是函数体中最后执行的语句并且它的返回值不属于表达式一部分时, 这个递归就是尾递归。
现代的编译器就会发现这个特点, 生成优化的代码, 复用栈帧。 (张大胖学递归)[https://mp.weixin.qq.com/s/YpG9TvTCBus2FK6LbArvvw]

函数式编程圣经

要有不变量;函数应该是纯粹的(纯函数); 要有递归; 要有高阶函数

定期整理简历

到了年末才整理简历,去做总结有点晚了, 应该在每个项目结束以后就去整理,时刻提醒自己:要有成长,不能原地踏步。对于没入行的人来讲,简历上要有项目经验,对于已经工作了的人来说,简历上应该有亮点

黑客精神

据说每个程序员内心都有一个黑客梦,其实攻击者那叫骇客(Cracker),只要你有一颗不被束缚的心,你就是黑客(Hacker

H5 首屏优化

前端优化 webview 池 客户端缓存 离线包 缓存/预加载/并行

Docker 镜像

简单来说 Docker 镜像中没有内核,Docker 镜像中程序共享宿主机内核 图片来自:Docker镜像和VM镜像

Elasticsearch 聚合中的重要概念 - Buckets(桶)及Metrics(指标)

一个桶就是满足特定条件的一个文档集合:

  • 一名员工要么属于男性桶,或者女性桶。
  • 城市Albany属于New York州这个桶。
  • 日期2014-10-28属于十月份这个桶。

桶能够让我们对文档进行有意义的划分,但是最终我们还是需要对每个桶中的文档进行某种指标计算。分桶是达到最终目的的手段:提供了对文档进行划分的方法,从而让你能够计算需要的指标。

多数指标仅仅是简单的数学运算(比如,min,mean,max以及sum),它们使用文档中的值进行计算。
一个聚合就是一些桶和指标的组合。一个聚合可以只有一个桶,或者一个指标,或者每样一个。在桶中甚至可以有多个嵌套的桶。 很久之前,对于磁盘的了解,就知道一个很关键的指标MTBF,即相邻两次故障之间的平均工作时间,也称为平均故障间隔,这个值越大越好,越大意味着硬盘更不容易坏。 对于RAID,也是很相信,觉得大多数情况下,使用RAID,就能保证数据的安全性,几乎不会有数据丢失的风险。

Unrecoverable Read Error Rate (URE)

突然的,读到一篇对于RAID 6的文章 Why RAID 6 stops working in 2019,这是一篇2010年的文章,很遗憾到目前才读到。 这篇文章里提到了一个指标,叫URE,也就是Unrecoverable Read Error Rate,不可恢复读取错误,一般普通的桌面级别硬盘,这个指标的值为1 × 10^-14,意味着每读取10^14bit的数据,就有可能产生1bit的错误。

问题在于,这个错误是无法被检测和修复的。10^14bit,大约相当于12.5TB的数据,也就是说,每读取12.5TB的数据,就有可能产生一个错误的读取。而对于目前现在硬盘的容量越来越大,4TB,6TB硬盘的价格越来越低,这种现象会越来越严重。

在RAID5中,当整个集群有一块硬盘出现损坏需要替换时,需要进行重建,重建时,需要读取其他硬盘的数据,计算出替换的那块硬盘的数据,在重建过程中,除了需要考虑重建的时间之外,还要考虑的就是URE的影响,如果集群的容量足够大,比如超过10TB,那么,其实是有很大的概率出现读取错误的,而一旦读取出错,则RAID的重建就会失败,基本也就意味着,数据能恢复的可能性变得相当低了。所以在使用RAID5时,就需要考虑重建的问题。

不过对于企业级的硬盘,URE普遍能做到1×10^-15,就意味着大约能读取125TB的数据,容量有比较大的提升,对于SSD,这个值会更加优秀,有些SSD能达到1×10^-17甚至1×10^-18,能提供更好的数据安全性。

所以,稳定点的话,还是RAID10吧。

RAID10 VS RAID01

  • RAID10是先做镜象,然后再做条带。
  • RAID01则是先做条带,然后再做镜象。

RAID10比RAID01在安全性方面要强。从数据存储的逻辑位置来看,在正常的情况下RAID01和RAID10是完全一样的,而且每一个读写操作所产生的IO数量也是一样的,所以在读写性能上两者没什么区别。而当有磁盘出现故障时,在读的性能上面也将不同,RAID10的读性能将优于RAID01==。

RAID5 典型的存储选择是 2D+1P、4D+1P、8D+1P。

  • D 指数据块(data)
  • P 指校验块(parity)

aspose 转码命令

。。。。。

ImageMagick 安装依赖的主要类库

yum install libpng libpng-devel libtiff libtiff-devel libjpeg-turbo libjpeg-turbo-devel zlib