redis-sentinel 高可用方案实践

redis-sentinel 高可用方案实践

近期公司的一块核心业务使用redis作为配置转发中心,存在单点问题,考虑服务的可靠性。针对业务需求,我们确定了我们的需求: 异地跨机房容灾 故障自动切换 尽可能高的保证数据不丢失 针对以上需求,我们分别对redis主从复之,redis-cluster,redis-sentinel方案进行了调研,对比结果如下: 方案 数据可靠性 服务可靠性 风险 主从备份 高 无 出问题需要手动切换,中间推送的数据会丢失 redis-cluster 高 高 异地机房热备,机房间网络出问题的情况下会出现脑裂的问题;同时我们对redis-cluster无运维和使用经验 redis-sentinel 高 中 在master宕机后,客户端会通过api查询当前master归属,重连redis 使用内网同步,主从之间的数据丢失同步基本可以忽略,业务可以忍受 基于以上调研,我们放弃了主从备份方案,对redis-cluster及redis-sentinel方案进行了深入分析。redis-cluster采用主从备份、master选举的方式实现高可用,但在异地机房部署时,如配置不当,很容易引发脑裂问题;同时由于redis-cluster我们并没有成熟的运维经验,最终放弃了该方案,转向redis-sentinel。 redis-sentinel就像他的名字一样,他是一个哨兵,监控master状态,如果超过规定时间没有响应,则自动进行主从切换,期间会有一段时间(决定于具体的配置参数)redis集群无法提供服务 。原理类似mysql的MHA。redis-sentinel-server同时提供了一套接口,用于查询当前集群的状态,Java的客户端有完整的封装,php的扩展并没有提供相应功能,在github上有几个package,但由于太过复杂,不适合迁移到现有业务(没有使用命名空间和composer),所以基于phpredis-2.2.8封装了一个简易的redis-sentinel客户端,目前已在核心业务生产环境上稳定运行。Java客户端与php客户端通过查询redis-sentinel集群获得当前redis-master地址,进行连接,当集群发生主从切换时,客户端会进行重连。 php-redis-sentinel的demo代码如下: $sentinel_pool = new \Jenner\RedisSentinel\SentinelPool(); $sentinel_pool->addSentinel(‘127.0.0.1’, 26379); $sentinel_pool->addSentinel(‘127.0.0.1’, 26380); $address = $sentinel_pool->master(‘mymaster’); print_r($address); $redis = $sentinel_pool->getRedis(‘mymaster’); $info = $redis->info(); print_r($info); 参考资料: redis-sentinel: https://redis.io/topics/sentinel cluster-tutorial: https://redis.io/topics/cluster-tutorial cluster-spec: https://redis.io/topics/cluster-spec   原创文章,转载请注明: 转载自始终不够 本文链接地址: redis-sentinel 高可用方案实践

flume日志收集系统架构设计

flume日志收集系统架构设计

    flume是当下最为流行的日志收集工具,还有很多同类型的产品原理大多与flume相同,具体可参考:http://www.ttlsa.com/log-system/scribe-chukwa-kafka-flume-log-system-contrast/。本文会假设你已经了解了flume的原理、组件、配置及使用方法。如果对flume还不是很了解,建议先阅读官方文档:http://flume.apache.org/。我们在广告系统日志收集中大量的使用了flume,并逐渐替换了原来的日志收集脚本。本文分为两部分向大家介绍flume日志收集架构及运维与监控。 架构设计 首先讲一下我们的业务场景,前端agent大概几十台机器,负责线上服务,产生各种类型的日志。这些日志异构、流量差异较大、重要度不同。我们希望设计一套简单、可复用的日志收集系统架构。 我们的flume日志收集系统架构如下:     agent 前端负载机,nginx或java server产生日志,由本地flume采集。这里的业务日志,我们采用直接落盘再由flume spooling dir source采集的方式,虽然flume提供了其他socket的采集方式,但我们觉得这种方式的侵入性最小,比较符合我们的要求。这里我们使用了load_banlance的机制,避免由collector故障引起的本地日志堆积问题。 这里可能产生的问题就是source无故停止工作,造成日志堆积。解决办法: 通过metrics接口监控source stop时间(没办法监控到所有情况,有时source只是不断在处理异常,并没有stop) 监控source目录下文件数量,文件总大小,文件最后修改时间(最靠谱的办法) collector A 日志收集中异地机房备份。这里做异地机房备份主要考虑如果collector B集群(所有机器都在一个机房)网络故障无法提供服务,collector A能够继续为agents提供服务,待collector B集群恢复之后,collector A再将数据传递给collector B。这时,你需要分配足够的channel空间和event数量给colletor A做故障缓冲,我们设定的是20G,你可以根据实际情况设定,保证collector A能够给你保留足够的故障处理时间。这里可能遇到的问题就是channel的性能瓶颈,由于所有日志都会传到这个节点,且这个节点仅仅是用来做故障备份不宜花费太多成本(尽量不加机器),那么方向只能是尽量提高单机IO吞吐能力了,增加硬盘,不同channel写不同硬盘,或磁盘做raid提高写能力等。 collector B 日志收集中心主集群,这个集群负责最终的日志收集、格式化、持久化等任务,首先collect B会将数据写入kafka,同时会写一份本地备份,再将数据传递给hive collect进行hdfs 持久化。需要注意的是,我们的kafka sink是使用1.6.0版本内置的,期间发现在collector节点CPU利用率非常高,使用jprofiler分析发现kafka sink占用了大多数的CPU,DEBUG发现存在死循环,有用到的童鞋要注意了。由于官网的flume版本一直没有更新,所以这个bug也会一直存在直到1.6.1或1.7发布。collector B还做了一些扩展,例如: NginxSerializer,自动对nginx日志进行格式化,结果使用\t分隔 ChannelSelector,由于我们的日志种类非常多,如果每种日志都有单独的source,对于运维管理非常不方便。所以我们开发了ChannelSelector,原理是根据basename header(spooling dir source自动注入)将event路由到不同的channel,最终由不同的sink处理 TopicInterceptor,我们知道event写入kafka时,如果没有topic header,会写入默认topic,我们种类繁多的日志需要写入不同的topic,我们使用TopicInterceptor结合业务场景,对不同日志进行不同配置 在collector B集群,我们使用二次开发的safe-rolling-file-sink替代了官方的rolling-file-sink,该sink支持原子的文件移动、文件复制等功能。 hive collector     hive数据接收器,负责向hdfs写入数据。这里没有使用collector B直接向hdfs写入数据的理由是,我们不希望由于某一个sink个故障导致整个collector B集群阻塞;例如hdfs集群故障,暂时无法写入数据,collector B就会把要写入hdfs的数据缓存在channel中,当这个channel满了之后,由于同一份日志要写三个channel(kafka channel、local
Continue reading flume日志收集系统架构设计

redis应用场景与最佳实践

redis应用场景与最佳实践

    Redis作为当下最流行的内存Nosql数据库,有着诸多的应用场景。在不同的应用场景,对Redis的部署、配置以及使用方式都存在的不同地方。根据我的工作经验,把队列、缓存、归并、去重等应用场景的“最佳实践”整理如下。     本文中的所有代码,均可在github上找到:https://github.com/huyanping/RedisStudy 队列      Redis的list数据结构经常会被用作队列来使用,常用的方法有:lpop/rpop、lpush/rpush、llen、lindex等。由于Redis提供的list是一个双向链表,我们也可以把list当做栈来使用。使用Redis的list作为队列时,需要注意以下几个问题: 队列中的数据一般具有比较高的可靠性要求,Redis的持久化机制最好使用AOF方式,保证数据不丢失 同样由于对数据的可靠性要求较高,内存监控尤为重要,如果出现队列堆积内存用光造成无法提供服务的情况 如果只有一个消费者在消费队列,推荐使用lindex先读取消息,消费完之后在lpop扔掉,这样可以保证事务性,避免消息处理失败后消息丢失 如果是多个消费者在消费队列,消息处理失败的情况下可以将消息重新写入队列,前期是消息没有有序性要求     通过批量(multi)和并行的方式可以提高生产者和消费者的处理能力。批量处理可以减少网络通信量,同时减少Redis在不同任务间切换的开销。并行的好处就是当一个客户端在准备或处理数据时并且Redis空闲时,另一个客户端可以从Redis读取数据;这样可以尽量保证Redis始终保持在繁忙状态。     如果通过以上优化,仍然有队列堆积的情况,建议启动多个Redis实例。由于Redis是单线程模型,无法利用多核CPU,开启多个实例能够明显提升吐吞量。Redis的集群方案有很多种,也可以简单的在客户端使用hash算法实现。具体实现方案已经超出本文叙述范围,不再累赘。     基于Redis list的消息队列使用示例代码如下: <?php // 生产者 namespace jenner\redis\study\queue; use Jenner\SimpleFork\Process; use Jenner\SimpleFork\Queue\RedisQueue; class Producer extends Process { /** * start producer process */ public function run() { $queue = new RedisQueue(‘127.0.0.1’, 6379, 1); for
Continue reading redis应用场景与最佳实践

PHP管道通信

PHP管道通信

项目所有源码可在https://github.com/huyanping/simple-fork-php找到。simple-fork-php是php多进程编程框架,以下实现的管道队列为框架的一部分。 管道是IPC(进程通信)最古老的的形式;像在shell中我们使用的ls | wc -l统计文件数量,中间的|实际上就是一个无名管道,它能够将ls的标准输出重定向为wc -l的标准输入。 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。管道单独构成一种独立的文件系统,管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。(有点像队列) 无名管道只能由于有亲缘关系的父子进程之间通信,如果要在不同归属的进程之间通信,则只能使用命名管道。由于命名管道能够实现无名管道的功能,这里我们主要介绍命名管道。 命名管道的作用更像是队列。php提供了posix_mkfifo函数创建命名管道文件,然后可以使用流操作管道文件。 一段简单的代码如下: $pipe = posix_mkfifo(‘/tmp/test.pipe’); $pid = pcntl_fork(); if($pid==0){ fwrite($pipe, ‘test’); sleep(1); }else{ echo fread($pipe, 4); } 上面代码需要注意两个个问题,fread读取时必须指定读取的长度,如果读取到的长度比指定长度要小,则fread会阻塞(下面会介绍无阻塞的方式);另一个问题管道不会持久化数据,任何一方关闭管道都会造成管道内数据的丢失。 我们可以对管道做进一步封装,实现一个先进先出的流式通信组件(无协议): <?php /** * @author Jenner <hypxm@qq.com> * @blog http://www.huyanping.cn * @license https://opensource.org/licenses/MIT MIT * @datetime: 2015/11/24 16:29 */ namespace Jenner\SimpleFork\Queue; class Pipe { /** * @var resource */
Continue reading PHP管道通信

php-affinity:修改php进程cpu亲和性

php-affinity:修改php进程cpu亲和性

通过设置cpu亲和性,可以将进程绑定到一个cpu核心上。一般在如下场景我们可能会希望修改进程的cpu亲和性: 频繁的并行运算 线程可伸缩性测试(通过增加cpu核心数量模拟线性扩展) 运行时间敏感的,决定性的进程 向nginx,swoole等开源软件,都提供了设置cpu亲和性的方法。nginx官方建议worker数量等同于cpu核心数量也是为了将一个进程绑定到一个cpu核心上,同时避免进程切换带来的开销。 php本身并没有提供设置cpu亲和性的函数,我们可以通过两种方式实现: 调用系统命令,修改当前进程cpu亲和性 编写php扩展,封装系统调用。 php-affinity是使用c编写的php扩展,项目地址:https://github.com/huyanping/php-affinity 提供了如下三个函数: setaffinity – 设置cpu亲和性 getaffinity – 获取cpu亲和性 getcpucores – 获取cpu核心数量 API如下: /** * set CPU affinity * * @param $cpu_id * @return bool */ function setaffinity($cpu_id){ $num = getcpucores(); if($cpu_id >= $num){ return false; } $set = system_call($cpu_id); if($set === -1){ return false; } return true;
Continue reading php-affinity:修改php进程cpu亲和性

spark 输出写入kafka[如何在spark中使用socket输出]

spark 输出写入kafka[如何在spark中使用socket输出]

spark提供了丰富的source源,我们可以从flume、kafka、各类文件系统中读取数据进行处理,但spark的输出组件提供的并不像输入那样丰富。spark就没有提供写入kafka的输出组件。 在spark streaming中,spark的输出结果很可能会写入一个队列供其他消费者消费,而无论这个消息服务是什么,大多情况下我们都需要使用socket与之进行通信。而在spark streaming中使用socket,我们需要考虑两个问题: socket在对象序列化后会关闭,反序列化时,需要重新激活(由于java中没有反序列化事件,这条很难实现) 重复创建链接会给server端带来压力,同时造成资源浪费 那么,我们需要解决的就是,在不能复用连接到情况下,如何控制socket数量。使用懒汉式单例模式,可以延迟初始化socket链接,对象创建时并不会初始化socket,真正调用的时候才会初始化。 一个使用redis的示例如下: public class SpiderCache implements Serializable { protected String host; protected int port; protected Jedis jedis; public SpiderCache(String host, int port) { this.host = host; this.port = port; } public void lazyInit() { if (jedis == null) { jedis = new Jedis(this.host, this.port); } } public String get(String
Continue reading spark 输出写入kafka[如何在spark中使用socket输出]

log-monitor:monitoring and alerting of active log based on tail command

log-monitor:monitoring and alerting of active log based on tail command

project home:https://github.com/huyanping/log-monitor An active log monitor based on the tail command. Why use log-monitor? Sometimes we want to know what is happenning when crontab task is running or what errors had been written into the php error log by php script, and we want to know quickly. log-monitor can help you to monitor the log
Continue reading log-monitor:monitoring and alerting of active log based on tail command

多进程共用mysql链接引发的一个问题

多进程共用mysql链接引发的一个问题

我们知道,每次fork操作系统都会复制进程的所有信息产生一个子进程(部分写时复制),其中就包括socket链接资源符。每个进程在PCB(Process Control Block)中都保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。实际上包括文件描述符,socket等资源符号,操作系统并没有真的复制,而是在父子进程之间复制了资源符的指针。 操作系统允许多个进程公用一个socket资源符,但是会有一定的风险。例如:两个进程同一时刻接收服务器发来的响应,就有可能抢读到对方的响应。 今天就遇到了与父子进程共用socket链接的一个问题,先上代码: $mysql = mysql_connect(‘127.0.0.1’, ‘root’, ‘123456’); $process_count = 0; while(true){ $pid = pcntl_fork(); if($pid<0){ echo “fork error” . PHP_EOL; }elseif($pid == 0){ $process_count++; if($process_count > 3){ pcntl_wait($status); $process_count–; } }else{ $mysql = mysql_connect(‘127.0.0.1’, ‘root’, ‘123456’); // do something exit; } } 我们看到,子进程会重新建立mysql链接,执行完任务后退出。退出时,链接自动关闭。最开始并没有出现问题,运行了过一会,出现了mysql server has gone away的错误。而重新建立mysql链接总是返回false。 后来与人讨论,才想起mysql_connect函数的第四个参数new_link默认为flase,也就是说,在参数相同的情况下,mysql会复用没有关闭的mysql链接。 这里我们回想下,上面的程序fork第一个子进程,由于new_link参数默认为false,所以实际上是复用了父进程的mysql链接,而当这个进程退出时,链接被关闭,mysql关闭会话线程。而父进程再次创建子进程时,同样继承了mysql资源服,而父进程并不知道mysql链接已经被第一个子进程关闭,所以被fork出来的第二个子进程同样不知道mysql链接不可用(服务端已关闭)。于是mysql_connect时依然重用了之前的mysql链接,必然是链接不成功的,返回false。 问题虽然不大,但是折腾了好久,最后总算是弄清楚了原因。 写代码在于折腾。 原创文章,转载请注明:
Continue reading 多进程共用mysql链接引发的一个问题

PHP异步编程简述

PHP异步编程简述

概述 异步编程,我们从字面上理解,可以理解为代码非同步执行的。异步编程可以归结为四种模式:回调、事件监听、发布/订阅、promise模式。我们最熟悉的两种模式是回调和事件监听,举两个最简单的javascript例子,一个ajax,一个点击事件的绑定: $.getJSON(“uri”, params, function(result) { do_something_with_data(result); }); $(“#id”).click(function(){ do_something_when_user_click_id(); }); 以上两个示例有一个共同的特点,就是把函数当做参数传递给另一个函数。被传递的函数可以被称作为闭包,闭包的执行取决于父函数何时调用它。 优势与劣势 异步编程具有以下优势: 解耦,你可以通过事件绑定,将复杂的业务逻辑分拆为多个事件处理逻辑 并发,结合非阻塞的IO,可以在单个进程(或线程)内实现对IO的并发访问;例如请求多个URL,读写多个文件等 效率,在没有事件机制的场景中,我们往往需要使用轮询的方式判断一个事件是否产生 异步编程的劣势: 异步编程的劣势其实很明显——回调嵌套。相信一部分人在写ajax的时候遇到过这样的场景: $.getJSON(“uri”, params, function(result_1) { $.getJSON(“uri”, result_1, function(result_2) { $.getJSON(“uri”, result_2, function(result_3) { do_something_with_data(result_3); }); });; }); 这样的写法往往是因为数据的依赖问题,第二次ajax请求依赖于第一次请求的返回结果,第三次ajax依赖于第二次。这样就造成深层次的回调嵌套,代码的可读性急剧下降。虽然有一些框架能够通过一些模式解决这样的问题,然并卵,代码的可读性相比同步的写法依然差很多。 异步编程的另一个劣势就是编写和调试的过程更加复杂,有时候你不知道什么时候你的函数才会被调用,以及他们被调用的顺序。而我们更习惯同步串行的编程方式。 然而,我相信一旦你开始使用异步编程,你一定会喜欢上这种方式,因为他能够带给你更多的便利。 PHP异步编程概述 在php语言中,异步的使用并不像javascript中那么多,归其原因主要是php一般是在web环境下工作,接收请求->读取数据->生成页面,这看起来天生就是一个串行的过程;所以,在php中,异步并没有广泛使用。 在javascript中的4中异步编程模式,均可以在php中实现。 回调: array_walk($arr, function($key, $value){ $value += 1; }); print_r($arr); 回调的方式,在大多情况下,代码仍然是顺序执行的(array_walk->print_r的顺序)。回调函数的意义在于被传递者可以调用回调函数对数据进行处理,这样的好处在于提供更好的扩展性和解耦。我们可以把这个回调函数理解为一个格式化器,处理相同的数据,当我传递一个json过滤器时,返回的结果可能是一个json压缩过的字符串,当我传递的是一个xml过滤器时,返回的结果可能是一个xml字符串(有点多态的思想)。 事件监听(定时器,时间事件): $loop = React\EventLoop\Factory::create();
Continue reading PHP异步编程简述

基于CURL_MULTI*系列函数的异步HTTP客户端

基于CURL_MULTI*系列函数的异步HTTP客户端

同步与异步 首先我们先通过一段简单的代码理解下什么是异步编程,再讲解如何使用异步HTTP客户端。 一般情况下,我们执行HTTP请求类似如下: $result = $http->get($url, $params); 如果我们有多个HTTP请求要执行,类似如下: $result_1 = $http->get($url_1, $params); $result_2 = $http->get($url_2, $params); $result_3 = $http->get($url_3, $params); $result_4 = $http->get($url_4, $params); … 我们看到上述代码是串行执行的。如果1、2、3、4中有任何一个请求发生了阻塞,则整个进程将发生阻塞,效率急剧下降。 异步的方式,可以让我们发出请求后不立即获取结果,等到请求到达时,再去处理。而请求可以是并行的,这种情况下,程序的执行时间,执行时间最长的一个请求。 比较常见的异步http客户端实现如下: $http->get($url, $params, function($data){ echo “I get the response data” . PHP_EOL; })->run(); 这里需要注意,run方法的实现一般是阻塞的,用于监听时间循环。如果你想同时请求多个,类似如下: $http->get($url, $params, function($data){ echo “I get the response data” . PHP_EOL; })->get($url, $params, function($data){
Continue reading 基于CURL_MULTI*系列函数的异步HTTP客户端