許多時候咱們說一款產品的設計是數據驅動的,是指許多產品方面的決策都是把用戶行爲量化後得出的。一個典例的例子就是註冊流程的設計,若是用戶須要填寫的註冊信息較多,通常就會分紅多個頁面,而產品設計師最關心的就是每一個頁面的流失率,從而不斷的對這個流程做調整以達到信息量與流失率之間的平衡。python
爲了可以量化用戶的行爲,前提是要將各類用戶事件都保存下來。其中最典型的事件包括user creation, page view和button click,但實際上還有許多其餘事件,好比用戶更改了狀態或是錄入了某些數據等等。目前有許多第三方的服務能夠幫助你作這方面的統計,國內有友盟,國外有Google Analytics和Mixpanel。但若是你記錄的事件數量很是龐大,或是對以後的數據分析有很是定製化的要求,那就要考慮本身構建事件分析的平臺,而這個過程當中最關鍵的一步就是如何存儲用戶事件。android
首先咱們來分析一下用戶事件存儲有哪些特性web
存儲這類數據的方法通常能夠分爲三類sql
後兩種方案有先天的技術優點,但維護成本高,而且其優點須要在數據量突破某個臨界點以後才能真正顯現。第一種方案看似毫無亮點,但對於創業型小團隊來講,卻有其價值在。由於關係數據庫你們都很熟悉,對於運維來講,沒有額外的維護成本。當數據量在TB如下時,若是正確地創建索引,查詢速度也是很是快,而且也能夠經過Sharding的方法作分佈式的擴容。Glow目前正處於從MySQL存儲到Redshift的轉型,因此今天咱們主要想分享一下用關係數據庫來存儲與分析用戶事件的一些經驗,咱們會在未來的博客中介紹後兩種系統(它們每每是共存的)數據庫
第一個要解決的問題是,咱們應該將全部的事件存在單一表中,仍是每一個事件存在單獨的表裏。二者有其各自的優點。好比後者,每一個事件單獨建表,表結構很是清晰,易於理解。但缺點是每次定義新事件都須要改動數據庫結構。咱們但願事件的定義是很是輕量的,因此在Glow咱們選擇了前者。前者的關鍵問題是,各類事件都有不一樣的屬性集合,難道把全部事件的全部屬性都放在表結構的定義中?這樣很快這個事件就會有成百上千的字段,對於存儲與查詢來講都很是的低效。咱們的作法是定義一組通用的字段用於事件屬性,並在代碼中定義映射關係。編程
咱們的事件表結構大體是這樣的api
CREATE TABLE `EventLog` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `event_time` bigint(20) NOT NULL, `event_name` varchar(255) NOT NULL, `user_id` bigint(20), `platform` tinyint(4), `app_version` varchar(20), `ip_address` varchar(20), `device_id` bigint(20), `data_1` bigint(20), `data_2` bigint(20), `data_3` bigint(20), `data_4` bigint(20), `data_5` bigint(20), `data_6` bigint(20), `text_1` varchar(255), `text_2` varchar(255), `text_3` varchar(255), `text_4` text, `text_5` text, `text_6` text, PRIMARY KEY (`id`), KEY `idx_event_id` (`event_time`, `event_id`), KEY `idx_event_and_platform` (`event_time`, `event_id`, `platform`), KEY `idx_event_and_version` (`event_time`, `event_id`, `version`), KEY `idx_user` (`user_id`), );
首先,咱們會記錄事件的名稱event_name
與時間event_time
,而後是全部事件共有的屬性user_id
, platform
, app_version
, ip_address
, device_id
。隨後的data_*
與text_*
則是用於各個事件的特有屬性。事件在代碼中的定義大體以下緩存
FORUM_NEW_TOPIC = {
'name': 'forum new topic', 'mapping': { 'room_id': 'data_1', 'subject': 'text_1', 'content': 'text_4', } }
這是論壇中發貼事件的定義,應該很容易看懂。討論區的IDroom_id
是整型,標題與貼子正文都是字符串,但正文極可能超出255長度的限制,因此被放入text_4
。再看一個更有趣些的例子服務器
SHARE_APP = {
'name': 'share app', 'mapping': { 'channel': {'field': 'data_1', 'enum': ['facebook', 'twitter', 'sms', 'email']}, 'message': 'text_1', } }
這是分享app的事件,其中分享渠道channel
是一個枚舉類型,因此被映射到了data_1
而不是text_1
。在存儲該類事件時,咱們會驗證事件中的channel
的值是否爲上述4個字串之一,而且只保存字符串的hash值。在從數據庫讀取該類事件時,當咱們解析data_1
字段的值時,會反向查找hash值對應的原始字串。在實際使用中,text_*
的字段的使用率是比較低的,由於大部分的用戶事件中的字符串都是枚舉類型。枚舉型的存儲佔用空間更小,查詢也更快,由於整數比較要明顯快於字串比較。微信
在Glow中,有一個事件定義文件,咱們稱爲事件的masterfile
,這個文件定義了Glow中全部的事件,由數據分析團隊管理與修改。另外有一個模塊專門負責將系統中接收到事件,根據masterfile
,轉化成正確的數據格式並存入數據庫。
以前也提到,用戶事件的數據遠多於其餘的生產環境數據。當單表的數據條數過大時,不管是查詢仍是插入性能都會降低,那麼如何擴容與保特性能呢?由於本質上這個事件數據是一個時間序列,因此第一步就是按時間維度分表。咱們把天天的數據放在一張單獨的表中,表的命名方式是event_log_YYYY_MM_DD
。這樣作有不少的好處
同時爲了方便Ad-hoc的查詢,咱們能夠把多個單日表合併成一個月視圖或是年視圖。
CREATE VIEW event_log_2014_01 AS SELECT * from event_log_2014_01_01 UNION ALL SELECT * from event_log_2014_01_02 ... UNION ALL SELECT * from event_log_2014_01_31;
對於事件的寫入,因爲時效性並不重要,因此咱們應儘可能將一段時間內的事件對象緩存在內存中,而後批量一次性的寫入。這樣對數據庫系統的負載會小不少。在實際的系統架構中,咱們爲用戶事件的收集與寫入單獨起一個Service進程,經過unix socket與web服務的主進程通訊。
事件數據庫的分佈式擴容很是容易,能夠經過user_id
作爲hash-key來分庫,也能夠隨機分庫。而後只需簡單的經過增長數據庫集羣中服務器的數量就能夠擴容了。
因爲咱們對事件的屬性作了映射與hash,同時作了按天分表以及分佈式的sharding,因此直接用SQL來對數據表查詢雖然可行,但並非很方便。咱們能夠把數據分析經常使用的一些查詢寫成API的形式,而且把前面提到的那些複雜性都封裝在API的實現中。在系統中,咱們稱這類API爲Metrics API
。在定義API接口的過程當中,咱們主要參考了Mixpanel的API接口定義
整個Metrics API的方法數量小於10個,如下是3個比較經常使用的API
def count(event_name, start_time, end_time, where=None): ''' 返回全部符合條件的事件的總數 >>> events('forum new topic', '2014/12/01', '2014/12/31', where={'room_id': 1}) 1321 2014年12月全部在討論區1中發貼事件發生的總次數爲1321。 ''' ... def group(event_name, property, start_time, end_time, where=None): ''' 對事件按某一個屬性進行分組,返回該屬性在這類事件中值的分佈 >>> group('share app', 'channel', '2014/12/01', '2014/12/31') { 'facebook' : 786, 'twitter' : 439, 'email' : 300, 'sms' : 257, } 2014年12月經過各個渠道分享app的次數統計 ''' ... def retention(start_time, time_unit, retention_length, born_event, retention_event, where=None): ''' 用戶的粘性分析,將某個時間段內誕生的用戶作爲實驗組,觀察這組用戶在以後的幾個時間段裏的活躍度 誕生事件由born_event決定,活躍事件由retention_avent決定 >>> retention('2014/12/01', 'week', 4, 'user created', 'app open', where={'platform': 'android'}) { 'cohort_size': 34032, 'retentions': [0.54, 0.42, 0.31, 0.25] } 以2014年12月1日爲始的那一週(12/01 - 12/07)在Android平臺上註冊的用戶作爲一個集合,共34032個用戶。 他們中在以後第一週(12/08 - 12/14)打開app的人數佔集合總數的54% 他們中在以後第二週(12/15 - 12/21)打開app的人數佔集合總數的42% 他們中在以後第三週(12/22 - 12/28)打開app的人數佔集合總數的31% 他們中在以後第四周(12/29 - 01/04)打開app的人數佔集合總數的25% ''' ...
數據分析團隊是Metrics API的主要用戶,他們95%以上的工做均可以經過這套API來完成。開發團隊則會經過併發或是緩存等方法,持續的優化API的性能。
此次與你們分享了基於關係數據庫的用戶事件存儲與分析,但願之後能將這套方案開源,但暫時尚未具體的時間計劃。在本文的開始,我提過目前Glow正在向用Redshift + Hadoop + Hive的平臺轉型,等這部分工做完成後再和你們分享經驗。
Head of Technology at Glow
這裏主要講一下關於代碼規範的相關問題,和在Android項目中如何利用一些工具進行規範和檢查。代碼規範不是一個Android項目特有的問題,因此前部份內容是不單針對Android的。 什麼是代碼規範? 代碼規範通常是指在編程過程當中的一系列規則規範。 通常來講代碼規範能夠分爲兩種。 一是編程語言自己在設計時所規定的一些原則,這類規則大部分都是強制的,像Python裏用縮進表示邏輯塊,Go裏用首字母大小寫表示可見度。 另一種是在一些組織約定的一些規範模式或我的在編寫代碼時的一些偏好,這種通常都是非強制的。好比大括號是放在方法名的同一行呢仍是另起一行,不一樣的人有不一樣的想法,我也不知道誰好,因此別問我。 假如是強制的,你們暫時也不能反抗,…
UIScrollView(包括它的子類 UITableView 和 UICollectionView)是 iOS 開發中最經常使用也是最有意思的 UI 組件,大部分 App 的核心界面都是基於三者之一或三者的組合實現。UIScrollView 是 UIKit 中爲數很少能響應滑動手勢的 view,相比本身用…
Glow 技術團隊博客 © 2016Proudly published with Ghost