阿里云CentOS 7上安装配置Docker

Docker 是一个开源工具,它可以让创建和管理 Linux 容器变得简单。容器就像是轻量级的虚拟机,并且可以以毫秒级的速度来启动或停止。Docker 帮助系统管理员和程序员在容器中开发应用程序,并且可以扩展到成千上万的节点。

1

这是一只鲸鱼,它托着许多集装箱。我们可以把宿主机可当做这只鲸鱼,把相互隔离的容器可看成集装箱,每个集装箱中都包含自己的应用程序。

Docker与传统虚拟区别

传统虚拟化技术的体系架构:

2

Docker技术的体系架构:

3

容器和 VM(虚拟机)的主要区别是:

  • 容器提供了基于进程的隔离,而虚拟机提供了资源的完全隔离。
  • 虚拟机可能需要一分钟来启动,而容器只需要一秒钟或更短。
  • 容器使用宿主操作系统的内核,而虚拟机使用独立的内核。

Doker 平台的基本构成

4

Docker 平台基本上由三部分组成:

  • 客户端:用户使用 Docker 提供的工具(CLI 以及 API 等)来构建,上传镜像并发布命令来创建和启动容器
  • Docker 主机:从 Docker registry 上下载镜像并启动容器
  • Docker registry:Docker 镜像仓库,用于保存镜像,并提供镜像上传和下载
  • 后面的文章会具体分析。

Docker 容器的状态机

5

一个容器在某个时刻可能处于以下几种状态之一:

  • created:已经被创建 (使用 docker ps -a 命令可以列出)但是还没有被启动 (使用 docker ps 命令还无法列出)
  • running:运行中
  • paused:容器的进程被暂停了
  • restarting:容器的进程正在重启过程中
  • exited:上图中的 stopped 状态,表示容器之前运行过但是现在处于停止状态(要区别于 created 状态,它是指一个新创出的尚未运行过的容器)。可以通过 start 命令使其重新进入 running 状态
  • destroyed:容器被删除了,再也不存在了

Docker 的安装

RedHat/CentOS必须要6.6版本以上,或者7.x才能安装docker,建议在RedHat/CentOS 7上使用docker,因为RedHat/CentOS 7的内核升级到了kernel 3.10,对lxc容器支持更好。

查看Linux内核版本(内核版本必须是3.10或者以上):

cat /proc/version

uname -a

lsb_release -a

##无法执行命令安装
yum install -y redhat-lsb

更新YUM源:

yum update

安装:

yum  install docker -y

检查版本:

docker -v

安装完成后,使用下面的命令来启动 docker 服务,并将其设置为开机启动:

service docker start
chkconfig docker on

下载官方的 CentOS 镜像:

docker pull centos

检查CentOS 镜像是否被获取:

docker images

下载完成后,你应该会看到:

[root@iZ2ze74fkxrls31tr2ia2fZ ~]# docker images centos
REPOSITORY       TAG        IMAGE ID     CREATED         SIZE
docker.io/centos latest    3fa822599e10    3weeks ago   203.5 MB

如果看到以上输出,说明你可以使用“docker.io/centos”这个镜像了,或将其称为仓库(Repository),该镜像有一个名为“latest”的标签(Tag),此外还有一个名为“3fa822599e10 ”的镜像 ID(可能您所看到的镜像 ID 与此处的不一致,那是正常现象,因为这个数字是随机生成的)。此外,我们可以看到该镜像只有 203.5 MB,非常小巧,而不像虚拟机的镜像文件那样庞大。

启动容器:

docker run -i -t -v /root/software/:/mnt/software/ 3fa822599e10 /bin/bash

docker run -ti ubuntu:14.04 /bin/bash

命令参数说明:
docker run <相关参数> <镜像 ID> <初始命令>

  • -i:表示以“交互模式”运行容器
  • -t:表示容器启动后会进入其命令行
  • -v:表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录>

Docker命令

我们可以把Docker 的命令大概地分类如下:

镜像操作:
    build     Build an image from a Dockerfile
    commit    Create a new image from a container's changes
    images    List images
    load      Load an image from a tar archive or STDIN
    pull      Pull an image or a repository from a registry
    push      Push an image or a repository to a registry
    rmi       Remove one or more images
    search    Search the Docker Hub for images
    tag       Tag an image into a repository
    save      Save one or more images to a tar archive 
    history   显示某镜像的历史
    inspect   获取镜像的详细信息

    容器及其中应用的生命周期操作:
    create    创建一个容器
    kill      Kill one or more running containers
    inspect   Return low-level information on a container, image or task
    pause     Pause all processes within one or more containers
    ps        List containers
    rm        删除一个或者多个容器
    rename    Rename a container
    restart   Restart a container
    run       创建并启动一个容器
    start     启动一个处于停止状态的容器
    stats     显示容器实时的资源消耗信息
    stop      停止一个处于运行状态的容器
    top       Display the running processes of a container
    unpause   Unpause all processes within one or more containers
    update    Update configuration of one or more containers
    wait      Block until a container stops, then print its exit code
    attach    Attach to a running container
    exec      Run a command in a running container
    port      List port mappings or a specific mapping for the container
    logs      获取容器的日志

    容器文件系统操作:
    cp        Copy files/folders between a container and the local filesystem
    diff      Inspect changes on a container's filesystem
    export    Export a container's filesystem as a tar archive
    import    Import the contents from a tarball to create a filesystem image

    Docker registry 操作:
    login     Log in to a Docker registry.
    logout    Log out from a Docker registry.

    Volume 操作
    volume    Manage Docker volumes

    网络操作
    network   Manage Docker networks

    Swarm 相关操作
    swarm     Manage Docker Swarm
    service   Manage Docker services
    node      Manage Docker Swarm nodes

    系统操作:
    version   Show the Docker version information
    events    持续返回docker 事件
    info      显示Docker 主机系统范围内的信息
# 查看运行中的容器
docker ps

# 查看所有容器
docker ps -a

# 退出容器
按Ctrl+D 即可退出当前容器【但退出后会停止容器】

# 退出不停止容器:
组合键:Ctrl+P+Q

# 启动容器
docker start 容器名或ID

# 进入容器
docker attach 容器名或ID

# 停止容器
docker stop 容器名或ID

# 暂停容器
docker pause 容器名或ID

#继续容器
docker unpause 容器名或ID

# 删除容器
docker rm 容器名或ID

# 删除全部容器--慎用
docker stop $(docker ps -q) & docker rm $(docker ps -aq)

#保存容器,生成镜像
docker commit 容器ID 镜像名称

#从 host 拷贝文件到 container 里面
docker cp /home/soft centos:/webapp

docker run与start的区别

docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start 即可。

docker run相当于执行了两步操作:将镜像放入容器中(docker create),然后将容器启动,使之变成运行时容器(docker start)。

6

而docker start的作用是,重新启动已存在的镜像。也就是说,如果使用这个命令,我们必须事先知道这个容器的ID,或者这个容器的名字,我们可以使用docker ps找到这个容器的信息。

7

因为容器的ID是随机码,而容器的名字又是看似无意义的命名,我们可以使用命令:

docker rename jovial_cori  centos

给这个容器命名。这样以后,我们再次启动或停止容器时,就可以直接使用这个名字:

docker [stop] [start]  new_name

而要显示出所有容器,包括没有启动的,可以使用命令:

docker ps -a

Docker配置

更改存储目录:

#复制docker存储目录
rsync -aXS /var/lib/docker/. /home/docker

#更改 docker 存储文件目录
ln -s  /home/docker  /var/lib/docker

获取IP:

docker inspect <container id>

要获取所有容器名称及其IP地址只需一个命令:

docker inspect -f '{{.Name}} - {{.NetworkSettings.IPAddress }}' $(docker ps -aq)

docker inspect --format='{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq)

Docker 镜像加速器

注册个帐号

https://dev.aliyun.com/search.html

阿里云会自动为用户分配一个镜像加速器的地址,登录后进入”管理中心”–>”加速器”,里面有分配给你的镜像加速器的地址以及各个环境的使用说明。

镜像加速器地址:https://xxxxx.mirror.aliyuncs.com

如何配置镜像加速器

针对Docker客户端版本大于1.10.0的用户
您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器:

{
    "registry-mirrors": ["<your accelerate address>"]
}

重启Docker Daemon:

sudo systemctl daemon-reload
sudo systemctl restart docker

后续:
1,查看镜像
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1fef6edc3f23 docker.io/centos "bin/bash" About a minute ago Exited (0) 8 seconds ago affectionate_khorana
66f6e4ff1098 docker.io/centos "bin/bash" About a minute ago Exited (0) About a minute ago elastic_khorana
a44b68ea8265 75835a67d134 "/bin/bash" 7 minutes ago Exited (0) 2 minutes ago angry_meitner
d0c8cbacb8ce 75835a67d134 "/bin/bash" 25 minutes ago Exited (0) 13 minutes ago mystifying_perlman
2.更新镜像
docker commit #第一列容器ID centos:update
3.保存镜像
docker save centos:update > centos.tar
4.加载镜像
docker load < centos.tar



常见命令:

1,后台启动docker并且端口映射

docker run -itd -p 3390<物理机端口>:3390<容器端口> cd3c0fd5029a /bin/bash

2,进入docker

docker attach <容器ID>

3,退出docker,不关闭container

ctrl+p ctrl+q

4.更新镜像

docker commit <容器ID> centos:update<自己定义的名字:标签>

5.保存镜像

docker save centos:update > centos.tar

6.加载镜像

docker load < centos.tar

Mycat【数据库方式】实现全局序列号

说明:本文参考mycat官方提供的文档,结合自己的实践以及理解,做出如下整理,并附带一个分库分表的插入数据例子。
原理
在数据库中建立一张表,存放sequence名称(name),sequence当前值(current_value),步长(increment int类型每次读取多少个sequence,假设为K)等信息;
Sequence获取步骤:
1)当初次使用该sequence时,根据传入的sequence名称,从数据库这张表中读取current_value,和increment到MyCat中,并将数据库中的current_value设置为原current_value值+increment值;
2)MyCat将读取到current_value+increment作为本次要使用的sequence值,下次使用时,自动加1,当使用increment次后,执行步骤1)相同的操作.
3)MyCat负责维护这张表,用到哪些sequence,只需要在这张表中插入一条记录即可。若某次读取的sequence没有用完,系统就停掉了,则这次读取的sequence剩余值不会再使用。
配置方式
server.xml配置:

<system><property name=”sequnceHandlerType”>1</property></system>
1
注:sequnceHandlerType 需要配置为1,表示使用数据库方式生成sequence.
数据库配置:
1)创建sequence表

CREATE TABLE MYCAT_SEQUENCE (
name VARCHAR (50) NOT NULL comment “名称”,
current_value INT NOT NULL comment “当前值”,
increment INT NOT NULL DEFAULT 100 comment “步长”,
PRIMARY KEY (name)
) ENGINE = INNODB;
1
2
3
4
5
6
2)创建相关function

#取当前squence的值
DROP FUNCTION IF EXISTS mycat_seq_currval;
DELIMITER $$
CREATE FUNCTION mycat_seq_currval(seq_name VARCHAR(50))RETURNS VARCHAR(64) CHARSET ‘utf8′
BEGIN
DECLARE retval VARCHAR(64);
SET retval=’-999999999,NULL’;
SELECT CONCAT(CAST(current_value AS CHAR),’,’,CAST(increment AS CHAR)) INTO retval FROM
MYCAT_SEQUENCE WHERE NAME = seq_name;
RETURN retval;
END$$
DELIMITER ;

#设置 sequence 值
DROP FUNCTION IF EXISTS mycat_seq_setval;
DELIMITER $$
CREATE FUNCTION mycat_seq_setval(seq_name VARCHAR(50),VALUE INTEGER) RETURNS VARCHAR(64) CHARSET ‘utf8’
BEGIN
UPDATE MYCAT_SEQUENCE SET current_value = VALUE WHERE NAME = seq_name;
RETURN mycat_seq_currval(seq_name);
END$$
DELIMITER ;

#取下一个sequence的值
DROP FUNCTION IF EXISTS mycat_seq_nextval;
DELIMITER $$
CREATE FUNCTION mycat_seq_nextval(seq_name VARCHAR(50)) RETURNS VARCHAR(64) CHARSET ‘utf8′
BEGIN
UPDATE MYCAT_SEQUENCE SET current_value = current_value + increment
WHERE NAME = seq_name;
RETURN mycat_seq_currval(seq_name);
END$$
DELIMITER ;
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
3)sequence_db_conf.properties相关配置,指定sequence相关配置在哪个节点上:
例如:

COMPANY=dn3
1
注:COMPANY为表名,必须大写,dn3为schema.xml配置的dataNode节点。建议专门独立一个数据库,存放sequence表和相关的function,方便维护管理和隔离。

注意:MYCAT_SEQUENCE表和以上的3个function,需要放在同一个节点上。function请直接在具体节点的数据库上执行,如果执行的时候报:
you might want to use the less safe log_bin_trust_function_creators variable
需要对数据库做如下设置:
windows下my.ini[mysqld]加上log_bin_trust_function_creators=1
linux下/etc/my.cnf下my.ini[mysqld]加上log_bin_trust_function_creators=1
修改完后,即可在mysql数据库中执行上面的函数.
使用示例:

SELECT next value for MYCATSEQ_SAM_TEST
insert into sam_test(id_,name_) values(next value for MYCATSEQ_SAM_TEST,’test’);
# 数据库表定义了自增,在mycat也定义了主键和自增,可以用如下方式
insert into sam_test(name_) values(‘test’);
1
2
3
4
测试
1.配置schema.xml

<schema name=”TESTDB” checkSQLschema=”false” sqlMaxLimit=”100″>
<table name=”company” dataNode=”dn1,dn2″ rule=”companyRule” primaryKey=”id” autoIncrement=”true” />
</schema>

<dataNode name=”dn1″ dataHost=”localhost1″ database=”mycat_test” />
<dataNode name=”dn2″ dataHost=”localhost1″ database=”mycat_test2″ />
<dataNode name=”dn3″ dataHost=”localhost2″ database=”testmycat” />

<dataHost name=”localhost1″ maxCon=”1000″ minCon=”10″ balance=”0″
writeType=”0″ dbType=”mysql” dbDriver=”native” switchType=”1″ slaveThreshold=”100″>
<heartbeat>select user()</heartbeat>
<writeHost host=”hostM1″ url=”192.168.1.95:3306″ user=”admin” password=”admin”/>
<writeHost host=”hostM2″ url=”192.138.1.112:3306″ user=”root” password=”root”/>
</dataHost>
<!– 存放sequence数据库 –>
<dataHost name=”localhost2″ maxCon=”1000″ minCon=”10″ balance=”0″
writeType=”0″ dbType=”mysql” dbDriver=”native” switchType=”1″ slaveThreshold=”100″>
<heartbeat>select user()</heartbeat>
<writeHost host=”localhost2M2″ url=”192.138.1.112:3306″ user=”root” password=”root”/>
</dataHost>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2.配置server.xml

<property name=”sequnceHandlerType”>1</property><!– 1:使用数据库方式生成sequence –>
1
3.配置rule.xml

<tableRule name=”companyRule”>
<rule>
<columns>id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
<function name=”mod-long” class=”org.opencloudb.route.function.PartitionByMod”>
<!– how many data nodes –>
<property name=”count”>2</property>
</function>
1
2
3
4
5
6
7
8
9
10
4.配置sequence_db_conf.properties

COMPANY=dn3
1
5.数据库配置文件修改my.ini

log_bin_trust_function_creators=1
# 忽略大小写
lower_case_table_names=1
1
2
3
6.数据库表
1)分别到192.168.1.95的mycat_test数据库和mycat_test2数据库新建如下的表,由于是分库分表,所以两边都要创建。

DROP TABLE IF EXISTS `company`;
CREATE TABLE `company` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5
6
注:只有数据库和mycat都设置AUTO_INCREMENT才能通过mycat命令LAST_INSERT_ID()获取插入的id
2)到192.168.1.112的testmycat数据库中执行上面的创建sequence和function过程。
3)插入数据到MYCAT_SEQUENCE表

insert into MYCAT_SEQUENCE(name,current_value,increment) values(‘COMPANY’,19,5);
1
7.mycat测试
配置完之后,重启mycat
执行

insert into company(id,name) values (next value for MYCATSEQ_COMPANY,”test”)

insert into company(name) values (“test”)
1
2
3
插入数据成功后
执行

select LAST_INSERT_ID()
1
可以看到本次插入的id

小结
如果要获取插入数据后的id,必须同时在mysql和mycat设置表的自增。
sequence_db_conf.properties配置的表名必须大写。
存放sequence表和function在同一个数据库中,且只有一个。
以上【Sequence获取步骤】是mycat原理,注意理解。
———————
作者:黄晓杰Aries
来源:CSDN
原文:https://blog.csdn.net/u010956470/article/details/70837876
版权声明:本文为博主原创文章,转载请附上博文链接!

mycat配置及使用

Mycat数据库分库分表中间件

详细文档在http://www.mycat.io/

本次主要想做分库分表的操作,将mysql分别部署在不同的机器上,mycat作为Proxy。

安装非常简单,下载相应的包就行。但是需要安装最新版本的java


yum install java-1.8.0-openjdk-src.x86_64

之后需要进行三个配置文件的配置

1,server.xml


<property name="sequnceHandlerType">3</property>

 

2,schema.xml

<dataNode name=”dn1″ dataHost=”localhost1″ database=”tt” />
<dataNode name=”dn2″ dataHost=”localhost2″ database=”tt” />

<dataHost name=”localhost2″ maxCon=”1000″ minCon=”10″ balance=”0″
writeType=”0″ dbType=”mysql” dbDriver=”native” switchType=”1″ slaveThreshold=”100″>
<heartbeat>select user()</heartbeat>
<writeHost host=”hostS1″ url=”IP:PORT” user=”root”
password=”XXX” />
</dataHost>

<dataHost name=”localhost2″ maxCon=”1000″ minCon=”10″ balance=”0″
writeType=”0″ dbType=”mysql” dbDriver=”native” switchType=”1″ slaveThreshold=”100″>
<heartbeat>select user()</heartbeat>
<writeHost host=”hostS1″ url=”IP:PORT” user=”root”
password=”XXX” />
</dataHost>

3,rule.xml

<function name=”mod-long” class=”io.mycat.route.function.PartitionByMod”>
<!– how many data nodes –>
<property name=”count”>2</property>
</function>

然后就可以根据主键id平均请求到不同的mysql。作为水平扩展。这里自增主键的方式可以参考官方文档。我用0的时候总是偶数,分表非常不友好。

如何优雅的设计和使用缓存?

背景

在之前的文章中你应该知道的缓存进化史介绍了爱奇艺的缓存架构和缓存的进化历史。俗话说得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好这些工具,本篇将介绍如何利用好缓存。

1.确认是否需要缓存

在使用缓存之前,需要确认你的项目是否真的需要缓存。使用缓存会引入的一定的技术复杂度,后文也将会一一介绍这些复杂度。一般来说从两个方面来个是否需要使用缓存:

  1. CPU占用:如果你有某些应用需要消耗大量的cpu去计算,比如正则表达式,如果你使用正则表达式比较频繁,而其又占用了很多CPU的话,那你就应该使用缓存将正则表达式的结果给缓存下来。
  2. 数据库IO占用:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。笔者曾经有个服务,被很多其他服务调用,其他时间都还好,但是在每天早上10点的时候总是会报出数据库连接池连接不够的报警,经过排查,发现有几个服务选择了在10点做定时任务,大量的请求打过来,DB连接池不够,从而报出连接池不够的报警。这个时候有几个选择,我们可以通过扩容机器来解决,也可以通过增加数据库连接池来解决,但是没有必要增加这些成本,因为只有在10点的时候才会出现这个问题。后来引入了缓存,不仅解决了这个问题,而且还增加了读的性能。

如果并没有上述两个问题,那么你不必为了增加缓存而缓存。

2.选择合适的缓存

缓存又分进程内缓存和分布式缓存两种。很多人包括笔者在开始选缓存框架的时候都感到了困惑:网上的缓存太多了,大家都吹嘘自己很牛逼,我该怎么选择呢?

2.1 选择合适的进程缓存

首先看看几个比较常用的缓存的比较,具体原理可以参考你应该知道的缓存进化史:

比较项 ConcurrentHashMap LRUMap Ehcache Guava Cache Caffeine
读写性能 很好,分段锁 一般,全局加锁 好,需要做淘汰操作 很好
淘汰算法 LRU,一般 支持多种淘汰算法,LRU,LFU,FIFO LRU,一般 W-TinyLFU, 很好
功能丰富程度 功能比较简单 功能比较单一 功能很丰富 功能很丰富,支持刷新和虚引用等 功能和Guava Cache类似
工具大小 jdk自带类,很小 基于LinkedHashMap,较小 很大,最新版本1.4MB 是Guava工具类中的一个小部分,较小 一般,最新版本644KB
是否持久化
是否支持集群
  • 对于ConcurrentHashMap来说,比较适合缓存比较固定不变的元素,且缓存的数量较小的。虽然从上面表格中比起来有点逊色,但是其由于是jdk自带的类,在各种框架中依然有大量的使用,比如我们可以用来缓存我们反射的Method,Field等等;也可以缓存一些链接,防止其重复建立。在Caffeine中也是使用的ConcurrentHashMap来存储元素。
  • 对于LRUMap来说,如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个。
  • 对于Ehcache来说,由于其jar包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择Ehcache。笔者没怎么使用过这个缓存,如果要选择的话,可以选择分布式缓存来替代Ehcache。
  • 对于Guava Cache来说,Guava这个jar包在很多Java应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解Caffeine的情况下可以选择Guava Cache。
  • 对于Caffeine来说,笔者是非常推荐的,其在命中率,读写性能上都比Guava Cache好很多,并且其API和Guava cache基本一致,甚至会多一点。在真实环境中使用Caffeine,取得过不错的效果。

总结一下:如果不需要淘汰算法则选择ConcurrentHashMap,如果需要淘汰算法和一些丰富的API,这里推荐选择Caffeine。

2.2 选择合适的分布式缓存

这里选取三个比较出名的分布式缓存来作为比较,MemCache(没有实战使用过),Redis(在美团又叫Squirrel),Tair(在美团又叫Cellar)。不同的分布式缓存功能特性和实现原理方面有很大的差异,因此他们所适应的场景也有所不同。

比较项 MemCache Squirrel/Redis Cellar/Tair
数据结构 只支持简单的Key-Value结构 String,Hash, List, Set, Sorted Set String,HashMap, List,Set
持久化 不支持 支持 支持
容量大小 数据纯内存,数据存储不宜过多 数据全内存,资源成本考量不宜超过100GB 可以配置全内存或内存+磁盘引擎,数据容量可无限扩充
读写性能 很高 很高(RT0.5ms左右) String类型比较高(RT1ms左右),复杂类型比较慢(RT5ms左右)
  • MemCache:这一块接触得比较少,不做过多的推荐。其吞吐量较大,但是支持的数据结构较少,并且不支持持久化。
  • Redis:支持丰富的数据结构,读写性能很高,但是数据全内存,必须要考虑资源成本,支持持久化。
  • Tair: 支持丰富的数据结构,读写性能较高,部分类型比较慢,理论上容量可以无限扩充。

总结:如果服务对延迟比较敏感,Map/Set数据也比较多的话,比较适合Redis。如果服务需要放入缓存量的数据很大,对延迟又不是特别敏感的话,那就可以选择Tair。在美团的很多应用中对Tair都有应用,在笔者的项目中使用其存放我们生成的支付token,支付码,用来替代数据库存储。大部分的情况下两者都可以选择,互为替代。

3.多级缓存

很多人一想到缓存马上脑子里面就会出现下面的图:


Redis用来存储热点数据,Redis中没有的数据则直接去数据库访问。

在之前介绍本地缓存的时候,很多人都问我,我已经有Redis了,我干嘛还需要了解Guava,Caffeine这些进程缓存呢。我基本统一回复下面两个答案:

  1. Redis如果挂了或者使用老版本的Redis,其会进行全量同步,此时Redis是不可用的,这个时候我们只能访问数据库,很容易造成雪崩。
  2. 访问Redis会有一定的网络I/O以及序列化反序列化,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便进一步加快访问速度。这个思路并不是我们做互联网架构独有的,在计算机系统中使用L1,L2,L3多级缓存,用来减少对内存的直接访问,从而加快访问速度。

所以如果仅仅是使用Redis,能满足我们大部分需求,但是当需要追求更高的性能以及更高的可用性的时候,那就不得不了解多级缓存。

3.1使用进程缓存

对于进程内缓存,其本来受限于内存的大小的限制,以及进程缓存更新后其他缓存无法得知,所以一般来说进程缓存适用于:

  1. 数据量不是很大,数据更新频率较低,之前我们有个查询商家名字的服务,在发送短信的时候需要调用,由于商家名字变更频率较低,并且就算是变更了没有及时变更缓存,短信里面带有老的商家名字客户也能接受。利用Caffeine作为本地缓存,size设置为1万,过期时间设置为1个小时,基本能在高峰期解决问题。
  2. 如果数据量更新频繁,也想使用进程缓存的话,那么可以将其过期时间设置为较短,或者设置其较短的自动刷新的时间。这些对于Caffeine或者Guava Cache来说都是现成的API。

3.2使用多级缓存

俗话说得好,世界上没有什么是一个缓存解决不了的事,如果有,那就两个。

一般来说我们选择一个进程缓存和一个分布式缓存来搭配做多级缓存,一般来说引入两个也足够了,如果使用三个,四个的话,技术维护成本会很高,反而有可能会得不偿失,如下图所示:


利用Caffeine做一级缓存,Redis作为二级缓存。

  1. 首先去Caffeine中查询数据,如果有直接返回。如果没有则进行第2步。
  2. 再去Redis中查询,如果查询到了返回数据并在Caffeine中填充此数据。如果没有查到则进行第3步。
  3. 最后去Mysql中查询,如果查询到了返回数据并在Redis,Caffeine中依次填充此数据。

对于Caffeine的缓存,如果有数据更新,只能删除更新数据的那台机器上的缓存,其他机器只能通过超时来过期缓存,超时设定可以有两种策略:

  • 设置成写入后多少时间后过期
  • 设置成写入后多少时间刷新

对于Redis的缓存更新,其他机器立马可见,但是也必须要设置超时时间,其时间比Caffeine的过期长。

为了解决进程内缓存的问题,设计进一步优化:


通过Redis的pub/sub,可以通知其他进程缓存对此缓存进行删除。如果Redis挂了或者订阅机制不靠谱,依靠超时设定,依然可以做兜底处理。

4.缓存更新

一般来说缓存的更新有两种情况:

  • 先删除缓存,再更新数据库。
  • 先更新数据库,再删除缓存。 这两种情况在业界,大家对其都有自己的看法。具体怎么使用还得看各自的取舍。当然肯定会有人问为什么要删除缓存呢?而不是更新缓存呢?你可以想想当有多个并发的请求更新数据,你并不能保证更新数据库的顺序和更新缓存的顺序一致,那就会出现数据库中和缓存中数据不一致的情况。所以一般来说考虑删除缓存。

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

对于一个更新操作简单来说,就是先去各级缓存进行删除,然后更新数据库。这个操作有一个比较大的问题,在对缓存删除完之后,有一个读请求,这个时候由于缓存被删除所以直接会读库,读操作的数据是老的并且会被加载进入缓存当中,后续读请求全部访问的老数据。


对缓存的操作不论成功失败都不能阻塞我们对数据库的操作,那么很多时候删除缓存可以用异步的操作,但是先删除缓存不能很好的适用于这个场景。

先删除缓存也有一个好处是,如果对数据库操作失败了,那么由于先删除的缓存,最多只是造成Cache Miss。

4.2先更新数据库,再删除缓存(推荐)

如果我们使用更新数据库,再删除缓存就能避免上面的问题。但是同样的引入了新的问题,试想一下有一个数据此时是没有缓存的,所以查询请求会直接落库,更新操作在查询请求之后,但是更新操作删除数据库操作在查询完之后回填缓存之前,就会导致我们缓存中和数据库出现缓存不一致。

为什么我们这种情况有问题,很多公司包括Facebook还会选择呢?因为要触发这个条件比较苛刻。

  1. 首先需要数据不在缓存中。
  2. 其次查询操作需要在更新操作先到达数据库。
  3. 最后查询操作的回填比更新操作的删除后触发,这个条件基本很难出现,因为更新操作的本来在查询操作之后,一般来说更新操作比查询操作稍慢。但是更新操作的删除却在查询操作之后,所以这个情况比较少出现。

对比上面4.1的问题来说这种问题的概率很低,况且我们有超时机制保底所以基本能满足我们的需求。如果真的需要追求完美,可以使用二阶段提交,但是其成本和收益一般来说不成正比。

当然还有个问题是如果我们删除失败了,缓存的数据就会和数据库的数据不一致,那么我们就只能靠过期超时来进行兜底。对此我们可以进行优化,如果删除失败的话 我们不能影响主流程那么我们可以将其放入队列后续进行异步删除。

5.缓存挖坑三剑客

大家一听到缓存有哪些注意事项,肯定首先想到的是缓存穿透,缓存击穿,缓存雪崩这三个挖坑的小能手,这里简单介绍一下他们具体是什么以及应对的方法。

5.1缓存穿透

缓存穿透是指查询的数据在数据库是没有的,那么在缓存中自然也没有,所以,在缓存中查不到就会去数据库取查询,这样的请求一多,那么我们的数据库的压力自然会增大。

为了避免这个问题,可以采取下面两个手段:

  1. 约定:对于返回为NULL的依然缓存,对于抛出异常的返回不进行缓存,注意不要把抛异常的也给缓存了。采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,当然我们可以通过设置较短的超时时间来解决这个问题。

2. 制定一些规则过滤一些不可能存在的数据,小数据用BitMap,大数据可以用布隆过滤器,比如你的订单ID 明显是在一个范围1-1000,如果不是1-1000之内的数据那其实可以直接给过滤掉。


5.2缓存击穿

对于某些key设置了过期时间,但是其是热点数据,如果某个key失效,可能大量的请求打过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加。

为了避免这个问题,我们可以采取下面的两个手段:

  1. 加分布式锁:加载数据的时候可以利用分布式锁锁住这个数据的Key,在Redis中直接使用setNX操作即可,对于获取到这个锁的线程,查询数据库更新缓存,其他线程采取重试策略,这样数据库不会同时受到很多线程访问同一条数据。
  2. 异步加载:由于缓存击穿是热点数据才会出现的问题,可以对这部分热点数据采取到期自动刷新的策略,而不是到期自动淘汰。淘汰其实也是为了数据的时效性,所以采用自动刷新也可以。

5.3缓存雪崩

缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩。

为了避免这个问题,我们采取下面的手段:

  1. 增加缓存系统可用性,通过监控关注缓存的健康程度,根据业务量适当的扩容缓存。
  2. 采用多级缓存,不同级别缓存设置的超时时间不同,及时某个级别缓存都过期,也有其他级别缓存兜底。
  3. 缓存的过期时间可以取个随机值,比如以前是设置10分钟的超时时间,那每个Key都可以随机8-13分钟过期,尽量让不同Key的过期时间不同。

6.缓存污染

缓存污染一般出现在我们使用本地缓存中,可以想象,在本地缓存中如果你获得了缓存,但是你接下来修改了这个数据,但是这个数据并没有更新在数据库,这样就造成了缓存污染:


上面的代码就造成了缓存污染,通过id获取Customer,但是需求需要修改Customer的名字,所以开发人员直接在取出来的对象中直接修改,这个Customer对象就会被污染,其他线程取出这个数据就是错误的数据。要想避免这个问题需要开发人员从编码上注意,并且代码必须经过严格的review,以及全方位的回归测试,才能从一定程度上解决这个问题。

7.序列化

序列化是很多人都不注意的一个问题,很多人忽略了序列化的问题,上线之后马上报出一下奇怪的错误异常,造成了不必要的损失,最后一排查都是序列化的问题。列举几个序列化常见的问题:

  1. key-value对象过于复杂导致序列化不支持:笔者之前出过一个问题,在美团的Tair内部默认是使用protostuff进行序列化,而美团使用的通讯框架是thfift,thrift的TO是自动生成的,这个TO里面很多复杂的数据结构,但是将其存放到了Tair中。查询的时候反序列化也没有报错,单测也通过,但是到qa测试的时候发现这一块功能有问题,发现有个字段是boolean类型默认是false,把它改成true之后,序列化到tair中再反序列化还是false。定位到是protostuff对于复杂结构的对象(比如数组,List
    等等)支持不是很好,会造成一定的问题。后来对这个TO进行了转换,用普通的Java对象就能进行正确的序列化反序列化。
  2. 添加了字段或者删除了字段,导致上线之后老的缓存获取的时候反序列化报错,或者出现一些数据移位。
  3. 不同的JVM的序列化不同,如果你的缓存有不同的服务都在共同使用(不提倡),那么需要注意不同JVM可能会对Class内部的Field排序不同,而影响序列化。比如下面的代码,在Jdk7和Jdk8中对象A的排列顺序不同,最终会导致反序列化结果出现问题:
//jdk 7
class A{
    int a;
    int b;
}
//jdk 8
class A{
    int b;
    int a;
}
复制代码

序列化的问题必须得到重视,解决的办法有如下几点:

  1. 测试:对于序列化需要进行全面的测试,如果有不同的服务并且他们的JVM不同那么你也需要做这一块的测试,在上面的问题中笔者的单测通过的原因是用的默认数据false,所以根本没有测试true的情况,还好QA给力,将其给测试出来了。
  2. 对于不同的序列化框架都有自己不同的原理,对于添加字段之后如果当前序列化框架不能兼容老的,那么可以换个序列化框架。 对于protostuff来说他是按照Field的顺序来进行反序列化的,对于添加字段我们需要放到末尾,也就是不能插在中间,否则会出现错误。对于删除字段来说,用@Deprecated注解进行标注弃用,如果贸然删除,除非是最后一个字段,否则肯定会出现序列化异常。
  3. 可以使用双写来避免,对于每个缓存的key值可以加上版本号,每次上线版本号都加1,比如现在线上的缓存用的是Key_1,即将要上线的是Key_2,上线之后对缓存的添加是会写新老两个不同的版本(Key_1,Key_2)的Key-Value,读取数据还是读取老版本Key_1的数据,假设之前的缓存的过期时间是半个小时,那么上线半个小时之后,之前的老缓存存量的数据都会被淘汰,此时线上老缓存和新缓存他们的数据基本是一样的,切换读操作到新缓存,然后停止双写。采用这种方法基本能平滑过渡新老Model交替,但是不好的点就是需要短暂的维护两套新老Model,下次上线的时候需要删除掉老Model,增加了维护成本。

8. GC调优

对于大量使用本地缓存的应用,由于涉及到缓存淘汰,那么GC问题必定是常事。如果出现GC较多,STW时间较长,那么必定会影响服务可用性。这一块给出下面几点建议:

  1. 经常查看GC监控,如何发现不正常,需要想办法对其进行优化。
  2. 对于CMS垃圾收集器,如果发现remark过长,如果是大量本地缓存应用的话这个过长应该很正常,因为在并发阶段很容易有很多新对象进入缓存,从而remark阶段扫描很耗时,remark又会暂停。可以开启-XX:CMSScavengeBeforeRemark,在remark阶段前进行一次YGC,从而减少remark阶段扫描gc root的开销。
  3. 可以使用G1垃圾收集器,通过-XX:MaxGCPauseMillis设置最大停顿时间,提高服务可用性。

9. 缓存的监控

很多人对于缓存的监控也比较忽略,基本上线之后如果不报错然后就默认他就生效了。但是存在这个问题,很多人由于经验不足,有可能设置了不恰当的过期时间,或者不恰当的缓存大小导致缓存命中率不高,让缓存就成为了代码中的一个装饰品。所以对于缓存各种指标的监控,也比较重要,通过其不同的指标数据,我们可以对缓存的参数进行优化,从而让缓存达到最优化:


上面的代码中用来记录get操作的,通过Cat记录了获取缓存成功,缓存不存在,缓存过期,缓存失败(获取缓存时如果抛出异常,则叫失败),通过这些指标,我们就能统计出命中率,我们调整过期时间和大小的时候就可以参考这些指标进行优化。

10. 一款好的框架

一个好的剑客没有一把好剑怎么行呢?如果要使用好缓存,一个好的框架也必不可少。在最开始使用的时候大家使用缓存都用一些util,把缓存的逻辑写在业务逻辑中:


上面的代码把缓存的逻辑耦合在业务逻辑当中,如果我们要增加成多级缓存那就需要修改我们的业务逻辑,不符合开闭原则,所以引入一个好的框架是不错的选择。

推荐大家使用JetCache这款开源框架,其实现了Java缓存规范JSR107并且支持自动刷新等高级功能。笔者参考JetCache结合Spring Cache, 监控框架Cat以及美团的熔断限流框架Rhino实现了一套自有的缓存框架,让操作缓存,打点监控,熔断降级,业务人员无需关心。上面的代码可以优化成:


对于一些监控数据也能轻松从大盘上看到:


最后

想要真正的使用好一个缓存,必须要掌握很多的知识,并不是看几个Redis原理分析,就能把Redis缓存用得炉火纯青。对于不同场景,缓存有各自不同的用法,同样的不同的缓存也有自己的调优策略,进程内缓存你需要关注的是他的淘汰算法和GC调优,以及要避免缓存污染等。分布式缓存你需要关注的是他的高可用,如果其不可用了如何进行降级,以及一些序列化的问题。一个好的框架也是必不可少的,对其如果使用得当再加上上面介绍的经验,相信能让你很好的驾驭住这头野马——缓存。

作者:公众号_咖啡拿铁
链接:https://juejin.im/post/5b849878e51d4538c77a974a
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

[转]微博应对日访问量百亿级的缓存架构设计

作者介绍

陈波,新浪微博技术专家,《深入分布式缓存》作者。

注:本文转自中生代技术订阅号(ID:freshmanTechnology),经平台授权转载。

微博日活跃用户1.6亿+,每日访问量达百亿级,面对庞大用户群的海量访问,良好的架构且不断改进的缓存体系具有非常重要的支撑作用。本文将由新浪微博技术专家陈波老师,跟大家详细讲解那些庞大的数据都是如何呈现的。

本文大纲

1、微博在运行过程中的数据挑战

2、Feed平台系统架构

3、Cache架构及演进

4、总结与展望

数据挑战

Feed平台系统架构

Feed平台系统架构总共分为五层,最上面是端层,比如web端、客户端、大家用的IOS或安卓的一些客户端,还有一些开放平台、第三方接入的一些接口;下一层是平台接入层,不同的池子,主要是为了把好的资源集中调配给重要的核心接口,这样遇到突发流量的时候,就有更好的弹性来服务,提高服务稳定性。再下面是平台服务层,主要是Feed算法、关系等等。接下来是中间层,通过各种中间介质提供一些服务。最下面一层就是存储层。

1
Feed Timeline

大家日常刷微博的时候,比如在主站或客户端点一下刷新,最新获得了十到十五条微博,这是怎么构建出来的呢?

刷新之后,首先会获得用户的关注关系。比如他有一千个关注,会把这一千个ID拿到,再根据这一千个UID,拿到每个用户发表的一些微博。同时会获取这个用户的Inbox,就是他收到的特殊的一些消息,比如分组的一些微博、群的微博、下面的关注关系、关注人的微博列表。

拿到这一系列微博列表之后进行集合、排序,拿到所需要的那些ID,再对这些ID去取每一条微博ID对应的微博内容。如果这些微博是转发过来的,它还有一个原微博,会进一步取原微博内容。通过原微博取用户信息,进一步根据用户的过滤词对这些微博进行过滤,过滤掉用户不想看到的微博。

根据以上步骤留下的微博,会再进一步来看,用户对这些微博有没有收藏、点赞,做一些flag设置,还会对这些微博各种计数,转发、评论、赞数进行组装,最后才把这十几条微博返回给用户的各种端。

这样看来,用户一次请求得到的十几条记录,后端服务器大概要对几百甚至几千条数据进行实时组装,再返回给用户,整个过程对Cache体系强度依赖,所以Cache架构设计优劣会直接影响到微博体系表现的好坏。

2
Feed Cache架构

接下来我们看一下Cache架构,它主要分为六层。首先第一层是Inbox,主要是分组的一些微博,然后直接对群主的一些微博。Inbox比较少,主要是推的方式。

然后对于第二层的Outbox,每个用户都会发常规的微博,都会在它的Outbox里面去。根据存的ID数量,实际上分成多个Cache,普通的大概是200多条,如果长的大概是2000条。

第三层是一些关系,它的关注、粉丝、用户。

第四层是内容,每一条微博一些内容存在这里。

第五层就是一些存在性判断,比如某条微博我有没有赞过。之前有一些明星就说我没有点赞这条微博怎么显示我点赞了,引发了一些新闻。而这种就是记录,实际上她有在某个时候点赞过但可能忘记了。

最下面还有比较大的一层——计数,每条微博的评论、转发等计数,还有用户的关注数、粉丝数这些数据。

Cache架构及演进
1
简单KV数据类型

接下来我们着重讲一下微博的Cache架构演进过程。最开始微博上线时,我们是把它作为一个简单的KV数据类型来存储。我们主要采取哈希分片存储在MC池子里,上线几个月之后发现一些问题:有一些节点机器宕机或是其它原因,大量的请求会穿透Cache层达到DB上去,导致整个请求变慢,甚至DB僵死。

于是我们很快进行了改造,增加了一个HA层,这样即便Main层出现某些节点宕机情况或者挂掉之后,这些请求会进一步穿透到HA层,不会穿透到DB层。这样可以保证在任何情况下,整个系统命中率不会降低,系统服务稳定性有了比较大的提升。

对于这种做法,现在业界用得比较多,然后很多人说我直接用哈希,但这里面也有一些坑。比如我有一个节点,节点3宕机了,Main把它给摘掉,节点3的一些QA分给其他几个节点,这个业务量还不是很大,穿透DB,DB还可以抗住。但如果这个节点3恢复了,它又加进来之后,节点3的访问就会回来,稍后节点3因为网络原因或者机器本身的原因,它又宕机了,一些节点3的请求又会分给其他节点。这个时候就会出现问题,之前分散给其他节点写回来的数据已经没有人更新了,如果它没有被剔除掉就会出现混插数据。

实际上微博是一个广场型的业务,比如突发事件,某明星找个女朋友,瞬间流量就30%了。突发事件后,大量的请求会出现在某一些节点,会导致这些节点非常热,即便是MC也没办法满足这么大的请求量。这时MC就会变成瓶颈,导致整个系统变慢。

基于这个原因,我们引入了L1层,还是一个Main关系池,每一个L1大概是Main层的N分之一,六分之一、八分之一、十分之一这样一个内存量,根据请求量我会增加4到8个L1,这样所有请求来了之后首先会访问L1。L1命中的话就会直接访问,如果没有命中再来访问Main-HA层,这样在一些突发流量的时候,可以由L1来抗住大部分热的请求。对微博本身来说,新的数据就会越热,只要增加很少一部分内存就会抗住更大的量。

简单总结一下,通过简单KV数据类型的存储,我们实际上以MC为主的,层内HASH节点不漂移,Miss穿透到下一层去读取。通过多组L1读取性能提升,能够抗住峰值、突发流量,而且成本会大大降低。对读写策略,采取多写,读的话采用逐层穿透,如果Miss的话就进行回写。对存在里面的数据,我们最初采用Json/xml,2012年之后就直接采用Protocol Buffer格式,对一些比较大的用QuickL进行压缩。

2
集合类数据

刚才讲到简单的QA数据,那对于复杂的集合类数据怎么来处理?

比如我关注了2000人,新增一个人,就涉及到部分修改。有一种方式是把2000个ID全部拿下来进行修改,但这种对带宽、机器压力会很大。还有一些分页获取,我存了2000个,只需要取其中的第几页,比如第二页,也就是第十到第二十个,能不能不要全量把所有数据取回去。还有一些资源的联动计算,会计算到我关注的某些人里面ABC也关注了用户D。这种涉及到部分数据的修改、获取,包括计算,对MC来说实际上是不太擅长的。

各种关注关系都存在Redis里面取,通过Hash分布、储存,一组多存的方式来进行读写分离。现在Redis的内存大概有30个T,每天都有2-3万亿的请求。

在使用Redis的过程中,实际上还是遇到其他一些问题。比如从关注关系,我关注了2000个UID,有一种方式是全量存储,但微博有大量的用户,有些用户登陆得比较少,有些用户特别活跃,这样全部放在内存里成本开销是比较大的。所以我们就把Redis使用改成Cache,比如只存活跃的用户,如果你最近一段时间没有活跃,会把你从Redis里踢掉,再次有访问的时候再把你加进来。

这时存在一个问题,因为Redis工作机制是单线程模式,如果它加某一个UV,关注2000个用户,可能扩展到两万个UID,两万个UID塞回去基本上Redis就卡住了,没办法提供其他服务。所以我们扩展一种新的数据结构,两万个UID直接开了端,写的时候直接依次把它写到Redis里面去,读写的整个效率就会非常高。它的实现是一个long型的开放数组,通过Double Hash进行寻址。

我们对Redis进行了一些其他的扩展,大家可能也在网上看到过我们之前的一些分享,把数据放到公共变量里面,整个升级过程,我们测试1G的话加载要10分钟,10G大概要十几分钟以上,现在是毫秒级升级。

对于AOF,我们采用滚动的AOF,每个AOF是带一个ID的,达到一定的量再滚动到下一个AOF里去。对RDB落地的时候,我们会记录构建这个RDB时,AOF文件以及它所在的位置,通过新的RDB、AOF扩展模式,实现全增量复制。

3
其他数据类型-计数

接下来还有一些其他的数据类型,比如一个计数,实际上计数在每个互联网公司都可能会遇到,对一些中小型的业务来说,实际上MC和Redis足够用的,但在微博里计数出现了一些特点:单条Key有多条计数,比如一条微博,有转发数、评论数,还有点赞;一个用户有粉丝数、关注数等各种各样的数字。因为是计数,它的Value size是比较小的,根据它的各种业务场景,大概就是2-8个字节,一般4个字节为多,然后每日新增的微博大概十亿条记录,总记录就更可观了,然后一次请求,可能几百条计数要返回去。

4
计数器-Counter Service

最初是可以采取Memcached,但它有个问题,如果计数超过它内容容量时,会导致一些计数的剔除,宕机或重启后计数就没有了。另外可能有很多计数它为零,那这个时候怎么存,要不要存,存的话就占很多内存。微博每天上十亿的计数,光存0都要占大量的内存,如果不存又会导致穿透到DB里去,对服务的可溶性会存在影响。

2010年之后我们又采用Redis访问,随着数据量越来越大之后,发现Redis内存有效负荷还是比较低的,它一条KV大概需要至少65个字节,但实际上我们一个计数需要8个字节,然后Value大概4个字节,所以有效只有12个字节,还有四十多个字节都是被浪费掉的。这还只是单个KV,如果在一条Key有多个计数的情况下,它就浪费得更多了。比如说四个计数,一个Key 8个字节,四个计数每个计数是4个字节,16个字节大概需要26个字节就行了,但是用Redis存大概需要200多个字节。

后来我们通过自己研发的Counter Service,内存降至Redis的五分之一到十五分之一以下,而且进行冷热分离,热数据存在内存里,冷数据如果重新变热,就把它放到LRU里去。落地RDB、AOF,实现全增量复制,通过这种方式,热数据单机可以存百亿级,冷数据可以存千亿级。

整个存储架构大概是上图这样,上面是内存,下面是SSD,在内存里是预先把它分成N个Table,每个Table根据ID的指针序列,划出一定范围。任何一个ID过来先找到它所在的Table,如果有直接对它增增减减,有新的计数过来,发现内存不够的时候,就会把一个小的Table Dump到SSD里去,留着新的位置放在最上面供新的ID来使用。

有些人疑问说,如果在某个范围内,我的ID本来设的计数是4个字节,但是微博特别热,超过了4个字节,变成很大的一个计数怎么处理?对于超过限制的,我们把它放在Aux dict进行存放,对于落在SSD里的Table,我们有专门的IndAux进行访问,通过RDB方式进行复制。

5
其他数据类型-存在性判断

除了计数,微博还有一些业务,一些存在性判断。比如一条微博展现的,有没有点赞、阅读、推荐,如果这个用户已经读过这个微博了,就不要再显示给他。这种有一个很大的特点,它检查是否存在,每条记录非常小,比如Value1个bit就可以了,但总数据量巨大。比如微博每天新发表微博1亿左右,读的可能有上百亿、上千亿这种总的数据需要判断。怎么来存储是个很大的问题,而且这里面很多存在性就是0。还是前面说的,0要不要存?如果存了,每天就存上千亿的记录;如果不存,那大量的请求最终会穿透Cache层到DB层,任何DB都没办法抗住那么大的流量。

我们也进行了一些选型,首先直接考虑能不能用Redis。单条KV 65个字节,一个KV可以8个字节的话,Value只有1个bit,这样算下来每日新增内存有效率是非常低的。第二种我们新开发的Counter Service,单条KV Value1个bit,我就存1个byt,总共9个byt就可以了。这样每日新增内存900G,存的话可能就只能存最新若干天的,存个三天差不多快3个T了,压力也挺大,但比Redis已经好很多。

我们最终方案是自己开发Phantom,先采用把共享内存分段分配,最终使用的内存只用120G就可以。算法很简单,对每个Key可以进行N次哈希,如果哈希的某一个位它是1,那么进行3次哈希,三个数字把它设为1。把X2也进行三次哈希,后面来判断X1是否存在的时候,从进行三次哈希来看,如果都为1就认为它是存在的,如果某一个哈希X3,它的位算出来是0,那就百分百肯定是不存在的。

它的实现架构比较简单,把共享内存预先拆分到不同Table里,在里面进行开方式计算,然后读写,落地的话采用AOF+RDB的方式进行处理。整个过程因为放在共享内存里面,进程要升级重启数据也不会丢失。对外访问的时候,建Redis协议,它直接扩展新的协议就可以访问我们这个服务了。

6
小结

小结一下,到目前为止,我们关注了Cache集群内的高可用、扩展性、组件高性能,还有一个特别重要就是存储成本,还有一些我们没有关注到的,比如运维性如何,微博现在已经有几千差不多上万台服务器等。

7
进一步优化

8
服务化

采取的方案首先就是对整个Cache进行服务化管理,对配置进行服务化管理,避免频繁重启,另外如果配置发生变更,直接用一个脚本修改一下。

服务化还引入Cluster Manager,实现对外部的管理,通过一个界面来进行管理,可以进行服务校验。服务治理方面,可以做到扩容、缩容,SLA也可以得到很好的保障。另外,对于开发来说,现在就可以屏蔽Cache资源。

总结与展望

最后简单总结一下,对于微博Cache架构来说,我们从它的数据架构、性能、储存成本、服务化等不同方面进行了优化增强。欢迎对此有研究或有疑问的同行们留言,跟我们一起探讨。

 

 

编译php报错cc: 编译器内部错误:已杀死(程序 cc1) Please submit a full bug report, with preprocessed source if appropri

报错信息

7.0.23/ext/fileinfo/ -DPHP_ATOM_INC -I/usr/local/src/php-7.0.23/include -I/usr/local/src/php-7.0.23/main -I/usr/local/src/php-7.0.23 -I/usr/local/src/php-7.0.23/ext/date/lib -I/usr/include/libxml2 -I/usr/include/freetype2 -I/usr/local/src/php-7.0.23/ext/mbstring/oniguruma -I/usr/local/src/php-7.0.23/ext/mbstring/libmbfl -I/usr/local/src/php-7.0.23/ext/mbstring/libmbfl/mbfl -I/usr/local/src/php-7.0.23/ext/sqlite3/libsqlite -I/usr/local/src/php-7.0.23/ext/zip/lib -I/usr/local/src/php-7.0.23/TSRM -I/usr/local/src/php-7.0.23/Zend  -D_REENTRANT  -I/usr/include -g -O2 -fvisibility=hidden -pthread -DZTS   -c /usr/local/src/php-7.0.23/ext/fileinfo/libmagic/apprentice.c -o ext/fileinfo/libmagic/apprentice.lo
cc: 编译器内部错误:已杀死(程序 cc1)
Please submit a full bug report,
with preprocessed source if appropriate.
See <http://bugzilla.redhat.com/bugzilla> for instructions.
make: *** [ext/fileinfo/libmagic/apprentice.lo] 错误 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

解决方法:

这个原因是内存不足, 在linux下增加临时swap空间
step 1:
#sudo dd if=/dev/zero of=/home/swap bs=64M count=16
注释:of=/home/swap,放置swap的空间; count的大小就是增加的swap空间的大小,64M就是块大小,这里是64MB,所以总共空间就是bs*count=1024MB.这里分配空间的时候需要一点时间,等待执行完毕。

step 2:
# sudo mkswap /home/swap (可能会提示warning: don’t erase bootbits sectorson whole disk. Use -f to force,不用理会)
注释:把刚才空间格式化成swap各式
step 3:
#sudo swapon /home/swap
注释:使刚才创建的swap空间
step 4:执行你相关的操作,如make
如果创建了临时空间仍然提示 “g++: 内部错误:Killed (程序 cc1plus)”,可能分配的空间不够大,可继续分配更大的空间。

关闭:
step 1:sudo swapoff /home/swap
step 2:sudo rm /home/swap

ffmpeg-2.5.2 在centos 6.4 x86_64 上编译安装全过程

原始编译参数

./configure –extra-libs=-ldl –prefix=/opt/ffmpeg –enable-avresample –disable-debug –enable-nonfree –enable-gpl –enable-version3 –enable-libpulse –enable-libopencore-amrnb –enable-libopencore-amrwb –disable-decoder=amrnb –disable-decoder=amrwb –enable-libx264 –enable-libx265 –enable-libfdk-aac –enable-libvorbis –enable-libmp3lame –enable-libopus –enable-libvpx –enable-libspeex –enable-libass –enable-avisynth –enable-libsoxr –enable-libxvid –enable-libvo-aacenc

最终优化后编译参数

./configure –extra-libs=-ldl –prefix=/opt/ffmpeg –enable-avresample –disable-debug –enable-nonfree –enable-gpl –enable-version3 –enable-libpulse –enable-libopencore-amrnb –enable-libopencore-amrwb –disable-decoder=amrnb –disable-decoder=amrwb –enable-libx264 –enable-libfdk-aac –enable-libvorbis –enable-libmp3lame –enable-libopus –enable-libvpx –enable-libspeex –enable-libass –enable-avisynth –enable-libsoxr –enable-libxvid –enable-libvo-aacenc

为了不用编译每个包,添加如下yum源: 创建一个这样的文件: /etc/yum.repos.d/linuxtech.repo

[linuxtech]
name=LinuxTECH
baseurl=http://pkgrepo.linuxtech.net/el6/release/
enabled=1
gpgcheck=1
gpgkey=http://pkgrepo.linuxtech.net/el6/release/RPM-GPG-KEY-LinuxTECH.NET

  1. yasm/nasm not found or too old. Use –disable-yasm for a crippled build.
    sudo yum install yasm.x86_64 yasm-devel.x86_64
  2. ERROR: libass not found using pkg-config
    sudo yum install libass.x86_64 libass-devel.x86_64
  3. ERROR: libfdk_aac not found
    wget http://iweb.dl.sourceforge.net/project/opencore-amr/fdk-aac/fdk-aac-0.1.3.tar.gz
    tar xzf fdk-aac-0.1.3.tar.gz
    cd fdk-aac-0.1.3
    ./configure
    make
    sudo make install
  4. ERROR: libmp3lame >= 3.98.3 not found
    sudo yum install libmp3lame.x86_64 libmp3lame-devel.x86_64
  5. ERROR: libopencore_amrnb not found
    sudo yum install libopencore-amr-devel.x86_64 libopencore-amr.x86_64
  6. ERROR: libopencore_amrwb not found
    sudo yum install libopencore-amr-devel.x86_64 libopencore-amr.x86_64
  7. ERROR: opus not found using pkg-config sudo yum install opus.x86_64 opus-devel.x86_64
  8. ERROR: libpulse not found using pkg-config
    sudo yum install pulseaudio-libs.x86_64 pulseaudio-libs-devel.x86_64
  9. ERROR: libsoxr not found
    sudo yum install libsoxr-devel.x86_64 libsoxr.x86_64
  10. ERROR: speex not found using pkg-config
    sudo yum install speex.x86_64 speex-devel.x86_64
  11. ERROR: libvo_aacenc not found
    wget http://hivelocity.dl.sourceforge.net/project/opencore-amr/vo-aacenc/vo-aacenc-0.1.3.tar.gz
    tar xzf vo-aacenc-0.1.3.tar.gz
    cd vo-aacenc-0.1.3
    ./configure
    make
    sudo make install
  12. ERROR: libvorbis not found
    sudo yum install libvorbis-devel.x86_64 libvorbis.x86_64
  13. ERROR: libvpx decoder version must be >=0.9.1
    sudo yum install libvpx.x86_64 libvpx-devel.x86_64
  14. ERROR: libx264 not found
    git clone git://git.videolan.org/x264.git
    cd x264
    ./configure –enable-static
    make
    sudo  make install
    sudo ldconfig
  15. ERROR: x265 not found using pkg-config
    去掉编译参数: –enable-libx265
    ./configure –extra-libs=-ldl –prefix=/opt/ffmpeg –enable-avresample –disable-debug –enable-nonfree –enable-gpl –enable-version3 –enable-libpulse –enable-libopencore-amrnb –enable-libopencore-amrwb –disable-decoder=amrnb –disable-decoder=amrwb –enable-libx264 –enable-libfdk-aac –enable-libvorbis –enable-libmp3lame –enable-libopus –enable-libvpx –enable-libspeex –enable-libass –enable-avisynth –enable-libsoxr –enable-libxvid –enable-libvo-aacenc
  16. ERROR: libxvid not found
    sudo yum install libxvidcore-devel.x86_64 libxvidcore.x86_64
  17. ./ffmpeg: error while loading shared libraries: libvo-aacenc.so.0: cannot open shared object file: No such file or directory

    sudo vim /etc/ld.so.conf.d/local_lib.conf

    /usr/local/lib

    sudo ldconfig

[转]ffmpeg+ffserver搭建流媒体服务器

使用FFMpeg和FFServer来搭建流媒体服务器(在windows下实现)
一、基础知识
需要用到以下四个东西:
1. ffmpeg.exe
   负责媒体文件的transcode工作,把你服务器上的源媒体文件转换成要发送出去的流媒体文件。
2. ffserver.exe
   负责响应客户端的流媒体请求,把流媒体数据发送给客户端。
3. ffserver.conf
   ffserver启动时的配置文件,在这个文件中主要是对网络协议,缓存文件feed1.ffm(见下述)和要发送的流媒体文件的格式参数做具体的设定。ffserver.conf参数说明可以访问FFMPEG的官方文档查看。http://ffmpeg.org/ffserver.html
4. feed1.ffm
   可以看成是一个流媒体数据的缓存文件,ffmpeg把转码好的数据发送给ffserver,如果没有客户端连接请求,ffserver把数据缓存到该文件中。
可以在ffserver.conf文件中设置feed1.ffm文件的大小,当缓存数据超过这个大小,它会自动覆盖原始数据。
二、如何实现
1.配置ffserver.conf文件
 下面是一个示例:
HTTPPort 8090                       #绑定端口号
BindAddress 0.0.0.0             #绑定ip
MaxHTTPConnections 2000         #最大HTTP连接数
MaxClients 1000                 #最大客户端连接数
MaxBandwidth 1000               #最大带宽
CustomLog –                     #日志文件,“-”为直接打印
NoDaemon                        #默认全局参数
<Feed feed1.ffm>                #Feed:每一个输入建立一个Feed
    File tests/feed1.ffm             #Feed缓存文件
    FileMaxSize 10M               #缓存文件最大值
    ACL allow 127.0.0.1             #允许写入Feed的ip
</Feed>
#传递实时流
<Stream stream1.flv>               #Stream:每一个广播(转换后的视频流)的转码设置项目
    Format flv                      #视频流格式
    Feed feed1.ffm                  #视频流的种子来源
    VideoFrameRate 35              #视频帧频
    VideoBitRate 128                #视频比特率
    VideoSize 160×80                #视频帧大小
    AVOptionVideo flags +global_header
    AudioBitRate 24                 #音频比特率
    AudioChannels 1                 #音频声道
    AudioSampleRate 44100
    AVOptionAudio flags +global_header
</Stream>
#传递本地文件
<Stream stream2>
File “files/test.avi”
Format avi
</Stream>
<Stream stat.html>  #检查服务器状态,ffserver启动后,在web浏览器中输入地址:http://localhost:8090/stat.html,若能看到 ffserver Status 说明成功启动,通过内容可以看到在ffserver.conf配置的流信息等内容
Format status
</Stream>
2.启动流媒体服务器
a. 可以在ffmpeg.exe、ffserver.exe所在的路径下创建一个.bat文件,在里面写入:ffserver -f ffserver.conf
b. 也可以打开cmd.exe  cd到你的ffmpeg.exe、ffserver.exe所在的路径,输入:ffserver -f ffserver.conf
       执行完这一步,你就可以在web浏览器中输入地址:http://localhost:8090/stat.html,若能看到 ffserver Status 说明成功启动,通过内容可以看到在ffserver.conf配置的流信息等内容。
注意:如果传输的是本地文件,那么到这一步就可以打开客户端(vlc)请求视频流地址:打开vlc–>File–>OpenNetwork–>URL中填写“http://ip:8090/stream2”–>Open (ip为FFserver所在主机电脑的ip,如果是本机测试可以使用localhost;stream2为FFserver配置文件中Stream项的名字,本例中即为stream2)。
3.FFMpeg产生实时流(如果传输的是本地文件,则不需要执行该步骤)
a. 可以在ffmpeg.exe、ffserver.exe所在的路径下创建一个.bat文件,在里面写入:ffmpeg -i rtsp://192.168.1.111:8000 http://localhost:8090/feed1.ffm
b. 也可以打开cmd.exe  cd到你的ffmpeg.exe、ffserver.exe所在的路径,输入:ffmpeg -i rtsp://192.168.1.111:8000 http://localhost:8090/feed1.ffm
其中:
#-i:为输入参数选项
#rtsp://192.168……:为输入的RTSP视频流来源
#http://localho……:为FFserver配置文件中设置的Feed的监听地址和端口
4.实现播放
打开客户端(vlc)请求视频流地址:打开vlc–>File–>OpenNetwork–>URL中填写“http://ip:8090/stream1”–>Open (ip为FFserver所在主机电脑的ip,如果是本机测试可以使用localhost;stream2为FFserver配置文件中Stream项的名字,本例中即为stream1)。
三、总结
ffserver先于ffmpeg启动,它在启动的时候需要加参数-f指定其配置文件。ffserver启动后,feed1.ffm就会被创建,这时如果你打开feed1.ffm看看,会发现feed1.ffm开始的部分已经写入了内容,你可以找到关键字ffm以及向客户端传送流的配置信息,在feed1.ffm做缓冲用的时候,这些信息是不会被覆盖掉的,可以将它们理解为feed1.ffm文件头。
ffserver启动后,ffmpeg启动,它启动时加的一个关键参数就是“http://ip:8090/feed1.ffm”,其中ip是运行ffserver主机的ip,如果ffmpeg和ffserver都在同一系统中运行的话,用localhost也行。ffmpeg启动后会与ffserver建立一个连接(短暂的连接),通过这第一次的连接,ffmpeg从ffserver那里获取了向客户端输出流的配置,并把这些配置作为自己编码输出的配置,然后ffmpeg断开了这次连接,再次与ffserver建立连接(长久的连接),利用这个连接ffmpeg会把编码后的数据发送给ffserver。
如果你观察ffserver端的输出就会发现这段时间会出现两次HTTP的200,这就是两次连接的过程。
ffmpeg获取数据后,按照输出流的编码方式编码,然后发送给ffserver,ffserver收到ffmpeg的数据后,如果网络上没有播放的请求,就把数据写入feed1.ffm中缓存,写入时把数据加上些头信息然后分块,每块4096B(每块也有结构),当feed1.ffm的大小到了ffserver.conf中规定的大小后,就会从文件开始(跳过头)写入,覆盖旧的数据。直到网络上有播放的请求,ffserver从feed1.ffm中读取数据,发送给客户端。
                                 2.http://blog.csdn.net/vblittleboy/article/details/8549835
                         3.http://ffmpeg.org/ffserver.html
友情链接:FFMPEG和FFSERVER搭建流媒体服务器实例,可以根据自己的需求修改里面的ffserver.conf文件。

[转]阿里巴巴开源项目TAC——新的服务端开发模式尝试

尝试

TAC是一个基于java的微服务容器,提供从业务代码编写、编译、发布、jar动态加载、运行等一系列常用开发流程的支持,是天猫App在服务端开发模式下的新尝试。TAC和客户端框架Tangram结合,极大提高开发效率;TAC目前在天猫App、手机淘宝特价版广泛使用;

疼痛之初——产生背景

天猫App(以下简称猫客)首页从2015年的坑位运营走向2016年的全面个性化,当时猫客首页的个性化业务多大50多处。以首页为例,这个过程中除了接入导购链路的二方服务之外,接入了大量的三方服务;同时我们发现:

  • 首页应用越来越复杂、庞大,应用代码修改、编译部署耗费时间太长,大部分时间都是苦力劳动;
  • 线上定位问题周期太长,从问题发现到定位再到处理,需要经过客户端、服务端、下游各种服务提供方,链路太长影响业务快速支撑;
  • 大量服务的接入导致发布线上部署频繁,一个简单服务的接入,几行代码的修改导致首页应用重新开发编译部署,影响整个首页稳定性;

当时微服务的概念已经很火,同时阿里内部也开始大力推广docker(现在大部分应用都跑在docker上)。我们想过将庞大的首页应用拆分成微服务,但是和基础服务不同,前台业务变化快,修改频繁,拆分之后开发同学依然要面临各种服务接入等体力劳动,同时需要维护拆分之后的多个应用,反而增加了劳动成本,因此拆分成微服务的方式治标不治本;


解决之道——TAC

在此背景下TAC孕育而生,它提供低成本开发与发布流程、低成本搭建与维护开发环境、高稳定性保障;TAC通过热部署的方式使得研发同学够从苦力劳动中解放出来,回归到业务开发中去,同时一个基础服务接入之后,能够提供给多个业务使用;在此模式下,业务能够进行更细粒度的拆分,且故障隔离业务A的改动不会影响业务B;

在TAC的帮助下,频繁修改的新业务可快速上线,不会出现因为修改一个字段、几行代码就需要重新发布整个应用的情况;同时与tangram结合,实现页面卡片、坑位的快速调整;


经过近三年的沉淀,我们今日放出了开源版本,将集团版本的TAC剥离与阿里相关的中间件、网络、协议、部署环境等,保留其核心功能;开源版本提供了编译、热加载、运行的基础能力;

TAC开源版本

系统结构


  • 开源版本分两部分,tac容器和tac控制台,存储和通信都依赖redis。
  • 为了简便,容器与外部服务直接的交互只通过http进行;
  • 同时为了提升开发体验,提供了与gitlab集成的能力,用户可直接gitlab提交代码并在控制台操作发布;快速验证;

核心类加载器


上图是tac的类加载器结构,每个线上的微服务实例都通过一个新的classloader加载,同时为了方便用户扩展新的数据源,在AppClassLoader上扩展了一个classloader以加载第三方数据源(当然也可以直接在代码中扩展,见tac-infrastructure);

Quick Start —— 如何使用

  • 安装 redis
  • 运行 container
java -jar tac-container.jar
  • 运行 console 控制台
java -jar tac-console.jar --admin
  • 成功后可打开控制台
http://localhost:7001/#/tacMs/list
  • 代码开发
    • 添加 SDK 依赖
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>tac-sdk</artifactId>
    <version>${project.version}</version>
</dependency>
  • 编写代码
public class HelloWorldTac implements TacHandler<Object> {

    /**
     * 引入日志服务
     */
    private TacLogger tacLogger = TacServiceFactory.getLogger();

    /**
     * 编写一个实现TacHandler接口的类
     *
     * @param context
     * @return
     * @throws Exception
     */

    @Override
    public TacResult<Object> execute(Context context) throws Exception {

        // 执行逻辑
        tacLogger.info("Hello World");

        Map<String, Object> data = new HashMap<>();
        data.put("name", "hellotac");
        data.put("platform", "iPhone");
        data.put("clientVersion", "7.0.2");
        data.put("userName", "tac-userName");
        return TacResult.newResult(data);
    }
}
  • 本地编译、打包
  • 发布及测试

  • 正式发布
  • 线上验证
curl  http://localhost:8001/api/tac/execute/helloworld -s|json
{
  "success": true,
  "msgCode": null,
  "msgInfo": null,
  "data": {
    "helloworld": {
      "data": {
        "name": "hellotac",
        "clientVersion": "7.0.2",
        "userName": "tac-userName",
        "platform": "iPhone"
      },
      "success": true,
      "msCode": "helloworld"
    }
  },
  "hasMore": null,
  "ip": "127.0.0.1"
}

Advanced Usage: Admin Server

link:https://docs.hhvm.com/hhvm/advanced-usage/admin-server

The admin server allows the administrator of the HHVM server to query and control the HHVM server process. It is different and separate than the primary HHVM server that you specified with -m server or -m daemon.

To turn on the admin server, you specify the following options at the command line via -d or within your server.ini (or equivalent).

hhvm.admin_server.port=9001
hhvm.admin_server.password=SomePassword

The port can be any open port. And you should always specify a password to secure the admin port since you don’t want just anybody being able to control your server. In fact, you will probably want to put the admin server behind a firewall. You will specify the password with every request to the admin port.

The admin server uses the same protocol as the main server – so, if you’re using FastCGI mode, the admin server will also be FastCGI, and you will need to configure a front-end webserver (like nginx). If you are using Proxygen mode, the admin server will be an HTTP server.

Querying the Admin Server

Once you have set up your admin server, you can query it via curl.

curl http://localhost:9001/

will bring up a list of commands you can use to control and query your admin server.

The port associated with the curl command is the hhvm.admin_server port set above if you are using ProxygenIf you are using FastCGI, then the port will be the webserver port that is the front end to FastCGI.

Sending a Command

Use one of the commands listed with the curl sequence above, along with your password, to send a command to the admin server.

curl http://localhost:9001/compiler-id?auth=SomePassword

Further Reference

There is a good blog post discussing the admin server even further.