文档章节

B站直播:使用Golang重构,流量最大的推送功能

anoty
 anoty
发布于 2016/10/22 14:20
字数 2277
阅读 1528
收藏 14

1 悲剧直播推送功能



1.1 B站直播推送功能的困境


B站直播有个推送功能,就是这里,看到那个红色的数字没有,显示你关注的主播开播人数。

输入图片说明 然后每个进入B站的用户,不管是不是直播的观众、不管进入B站哪个页面、不管你要干啥,都要请求一次这个人数接口,直播服务表示:妈逼,就给老子几台土豆服务器,却要扛着跟主站一样PV,

输入图片说明

不仅仅是主站在使用这个功能,还有直播服务内部的各种推送心跳同样在使用这个功能,流量很大。

由于主站、直播对于UP主和主播关注是混在一起的,所以每次直播这边都要从一堆用户关注UP主中找到直播的主播,并且还要找到那个主播在直播,老的做法就是从缓存读各种数据,然后遍历,计算,然后输出,对缓存服务器、PHP服务器都造成了极大的压力,然后遇到大的活动,服务器分分钟都是:老子不想干了的节奏。然后大的活动每次都会把推送能关掉,来保证活动正常进行。

1.2 穷则思变的重构


你们以为大佬们一开始就同意我的Golang重构方案吗?你们啊

太年轻

我苦口婆心的跟大佬们诉说我的方案是多么适合这个业务,然后我Golang技术有多好(无耻笑)、多靠谱,加上在弹幕服务器部门做了一段时间Golang的兼职(没错,是我舔着脸要去的),做了些大流量的功能,他们终于同意了,呵呵,是时候展示真正的技术了(请脑补小黄毛EZ配音)。

1.3 使用Golang重构基本思路


  • 用Golang进程内存替换Memcache,减少网络io。
  • 让Golang计算数据,PHP通过RPC获取计算好数据,然后组装下房间标题,用户头像数据等。

没错,Golang做的就是个数据中间件。



2 重构踩坑路



2.1 解决PHP和Golang的通信问题


2.1.1 PHP的Yar RPC


最开始想要使用鸟哥PHP的Yar RPC扩展,虽然Yar在php手册里并没有说明Yar支持tcp协议的通信方式,但是我通过阅读Yar的源码发现,其实它是支持tcp协议的通信方式。 yar_client.c

PHP_METHOD(yar_client, __construct) {
	zend_string *url;
	zval *options = NULL;

    if (zend_parse_parameters_throw(ZEND_NUM_ARGS(), "S|a!", &url, &options) == FAILURE) {
        return;
    }

    zend_update_property_str(yar_client_ce, getThis(), ZEND_STRL("_uri"), url);

	if (strncasecmp(ZSTR_VAL(url), "http://", sizeof("http://") - 1) == 0
			|| strncasecmp(ZSTR_VAL(url), "https://", sizeof("https://") - 1) == 0) {
	} else if (strncasecmp(ZSTR_VAL(url), "tcp://", sizeof("tcp://") - 1) == 0) {
		zend_update_property_long(yar_client_ce, getThis(), ZEND_STRL("_protocol"), YAR_CLIENT_PROTOCOL_TCP);
	} else if (strncasecmp(ZSTR_VAL(url), "unix://", sizeof("unix://") - 1) == 0) {
		zend_update_property_long(yar_client_ce, getThis(), ZEND_STRL("_protocol"), YAR_CLIENT_PROTOCOL_UNIX);
	} else {
		php_yar_client_trigger_error(1, YAR_ERR_PROTOCOL, "unsupported protocol address %s", ZSTR_VAL(url));
		return;
	}

	if (options) {
    	zend_update_property(yar_client_ce, getThis(), ZEND_STRL("_options"), options);
	}
}

客户端OK,那么开始找服务端,面向Github编程的时候到了,果真已经有人实现Golang的Yar服务端 goyar,嘿嘿,把作者写的demo跑一下,发现没什么问题,然后我习惯性的用wireshark抓包看看,Yar client和server之间的通信,愕然发现,Yar client不复用任何tcp连接,即使是同一个Yar client对象,每次请求都是不复用tcp连接的(大写的懵逼脸),虽然不太清楚鸟哥这么实现真实意图,个人猜测可能是为了Yar client异步并发请求,防止数据错误,才这么设计的。代码就不贴了,有兴趣的同学可以去goyar,自己跑下demo验证。

Yar RPC这条路不通了,然后我开始研究其他的方式。


2.1.2 JSON-RPC


通过阅读Golang jsonrpc包源码、文档、JSON-RPC协议文档、Golang实现jsonrpc的server和client通信抓包,我发现JSON-RPC协议,仅仅是通过固定格式的json字符串来通信的,而且没有什么包头、包长、结束符之类的设置,真是简单粗暴(微笑脸),贴一段Go jsonrpc server 简单看下。

// 这里就是请求的结构体
type serverRequest struct {
	Method string           `json:"method"`
	Params *json.RawMessage `json:"params"`
	Id     *json.RawMessage `json:"id"`
}

// 这里就是返回的结构体
type serverResponse struct {
	Id     *json.RawMessage `json:"id"`
	Result interface{}      `json:"result"`
	Error  interface{}      `json:"error"`
}

func (c *serverCodec) ReadRequestHeader(r *rpc.Request) error {
	c.req.reset()
	if err := c.dec.Decode(&c.req); err != nil {
		return err
	}
	r.ServiceMethod = c.req.Method

	// JSON request id can be any JSON value;
	// RPC package expects uint64.  Translate to
	// internal uint64 and save JSON on the side.
	c.mutex.Lock()
	c.seq++
	c.pending[c.seq] = c.req.Id
	c.req.Id = nil
	r.Seq = c.seq
	c.mutex.Unlock()

	return nil
}

func (c *serverCodec) ReadRequestBody(x interface{}) error {
	if x == nil {
		return nil
	}
	if c.req.Params == nil {
		return errMissingParams
	}
	// JSON params is array value.
	// RPC params is struct.
	// Unmarshal into array containing struct for now.
	// Should think about making RPC more general.
	var params [1]interface{}
	params[0] = x
	return json.Unmarshal(*c.req.Params, &params)
}

var null = json.RawMessage([]byte("null"))

func (c *serverCodec) WriteResponse(r *rpc.Response, x interface{}) error {
	c.mutex.Lock()
	b, ok := c.pending[r.Seq]
	if !ok {
		c.mutex.Unlock()
		return errors.New("invalid sequence number in response")
	}
	delete(c.pending, r.Seq)
	c.mutex.Unlock()

	if b == nil {
		// Invalid request so no id. Use JSON null.
		b = &null
	}
	resp := serverResponse{Id: b}
	if r.Error == "" {
		resp.Result = x
	} else {
		resp.Error = r.Error
	}
	return c.enc.Encode(resp)
}

这里有实现代码PHP和Golang通过JSON- RPC通信,顺便说一下,PHP socket扩展的性能还是很不错的,i5 CPU、8G内存的macOS可以单连接达到2-3w QPS,Golang服务的QPS后面再说。

至于连接复用,只需要简单是用下单例模式,保证用户一次http请求到结束,用的是一个tcp连接即可,在请求结束后,释放这个连接。


2.2 数据结构的选择


2.2.1 Golang map

将主播的直播关播数据和用户关注数据用key value的形式分别放到map里,写了第一个版本,然后用Golang写个一个压测工具,1000并发、每个连接请求1000次,每次测试程序都会crash,错误

fatal error: concurrent map writes
fatal error: concurrent map read and map write

当时我是懵逼的,我写map只有一个goroutine在写,其他都在读啊,怎么会这样,OK,面向stackoverflow编程的时候到了,看到有人说Go 1.5的时候,map是可以脏读的(推送服务并不要求100%的准备,允许有脏数据),但是Go 1.6不允许这么做了……日了狗了……


2.2.2 syncmap


又要面向Github编程,有人实现并发map,syncmap,原理很简单,就是使用Go sync包的读写锁功能,来实现并发安全,而且还实现了数据分片,看的代码,写的不错,做了下测试,性能不错,但是这个syncmap key只能用string,我做了简单的修改key支持int

func (m *SyncMap) locate(key interface{}) *syncMap {
	ik, ok := key.(int)
	if ok {
		return m.shards[uint32(ik) & uint32((m.shardCount - 1))]
	}
	sk := key.(string)
	return m.shards[bkdrHash(sk) & uint32((m.shardCount - 1))]
}


3 数据存储的选择



3.1 MySQL和LevelDB

进程启动的时候,从MySQL全量读取主播数据放到内存,然后并异步存到Go版本的LevelDB,用户关注数据在用户初次访问的时候,从主站API获取并缓存到LevelDB,然后在程序重启更新的时候,可以做到快速重启(因为仅读本地数据),对用户的影响时间可以降到最小。

LevelDB的主要作用就是数据冷备,在进程重启的时候使用,减少对数据库的压力。

但是LevelDB能做的不仅仅如此,LevelDB和Go能轻松实现一个类似于Redis(有人已经实现将LevelDB整合到Redis里)的服务,还有待挖掘。



4 容灾备份



进程启动的时候会注册到zookeeper的Ephemeral类型的node,在程序重启、宕机的时候,自动将新的配置发送到PHP服务器,做到无缝切换。



5 重构后的效果



5.1 Golang 的性能

重构完成后,我对这个中间层服务做了个压测(客户端??自己写呀),1000并发,1000请求,i5 CPU,8G内存debain linux pc机,达到了14W多的QPS,每个核的使用率稳定在70%左右,线上服务器24核服务器请自动心算*X就可以大致估算出来,并考虑服务器CPU比普通的PC的CPU高到不知道哪里去了,呵呵,不小心又续……。

5.2 PHP接口耗时

具体监控数据不方便贴出来,我简单说下:推送接口耗时减少了2/3还多,而且稳定性也提高了不少,而且这些接口的日访问量是以亿为单位的。



布道一波



Golang 现在已经拥有完善的社区环境,很多东西都能面向Github编程,内置包功能完善,学习成本很低,简直就是编译强类型语言中的PHP。

更多架构、PHP、GO相关踩坑实践技巧请关注我的公众号:PHP架构师

© 著作权归作者所有

anoty
粉丝 29
博文 48
码字总数 28431
作品 0
浦东
私信 提问
加载中

评论(1)

BarryZ
BarryZ
让我看看报道哪里出现了偏差...
gopush-cluster 1.0 发布,实时消息推送集群

gopush-cluster 1.0 发布,此版本合并 protocol 分支 到 master。 主要更新内容如下: * 避免多次json序列化,优化客户端协议(节省流量高大5倍之多),新老协议完全兼容 * 支持web 负载均衡...

LoveSai
2014/04/29
8.6K
14
bilibili高并发实时弹幕系统的实战之路

高并发实时弹幕是一种互动的体验。对于互动来说,考虑最多的地方就是:高稳定性、高可用性以及低延迟这三个方面。 高稳定性,为了保证互动的实时性,所以要求连接状态稳定; 高可用性,相当于...

www19
2016/10/21
0
0
「泛娱乐+直播」技术最佳实践,火热报名中! | 七牛架构师实践日第十期

这是一个全民直播的时代。 这是一个泛娱乐化的时代。 如何借助直播工具, 让泛娱乐产业走得更深、更远是当前企业的关注点。 本期架构师实践日,让我们从技术出发,看看: 泛娱乐行业新秀、最受...

七牛云
2016/07/22
301
0
「泛娱乐+直播」技术最佳实践,火热报名中! | 七牛架构师实践日第十期

这是一个全民直播的时代。 这是一个泛娱乐化的时代。 如何借助直播工具, 让泛娱乐产业走得更深、更远是当前企业的关注点。 本期架构师实践日,让我们从技术出发,看看: 泛娱乐行业新秀、最受...

七牛云
2016/07/22
7
0
聊聊,直播架构

分享一个直播架构 要说架构,无图言屌 直播架构整体分为以下几个部分: MySQL数据库。 缓存(memcache,redis)。 消息队列(kafka)。 队列消费服务。 配置管理zookeeper。 Golang-RPC中间件...

anoty
2016/09/27
91
0

没有更多内容

加载失败,请刷新页面

加载更多

Mysql的sql_mode模式

sql_mode 是一个很容易被忽视的配置,宽松模式下可能会被输入一些非准确数据,所以生产环境下会要求为严格模式,为了保持生产环境和开发环境,测试环境一致性,我们开发环境和测试环境也要配...

贾峰uk
38分钟前
4
0
Qt程序打包发布方法(使用官方提供的windeployqt工具)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/toTheUnknown/article/details/81748179 如果使用到了Qt ...

shzwork
今天
7
0
MainThreadSupport

MainThreadSupport EventBus 3.0 中的代码片段. org.greenrobot.eventbus.MainThreadSupport 定义一个接口,并给出默认实现类. 调用者可以在EventBus的构建者中替换该实现. public interface ...

马湖村第九后羿
今天
3
0
指定要使用的形状来代替文字的显示

控制手机键盘弹出的功能只能在ios上实现,安卓是实现不了的,所以安卓只能使用type类型来控制键盘类型,例如你要弹出数字键盘就使用type="number",如果要弹出电话键盘就使用type="tel",但这...

前端老手
今天
8
0
总结:Raft协议

一、Raft协议是什么? 分布式一致性算法。即解决分布式系统中各个副本数据一致性问题。 二、Raft的日志广播过程 发送日志到所有Followers(Raft中将非Leader节点称为Follower)。 Followers收...

浮躁的码农
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部