详解iOS应用程序内使用IAP/StoreKit付费、沙盒(SandBox)测试、创建测试账号流程!

原文地址:http://blog.csdn.net/xiaominghimi/article/details/6937097

 

终于在11月公司的游戏即将上线了,那么对于iOS游戏来说当今都是内置道具收费属于主流,那么我们的游戏也是内置收费,所以Himi这里分享给大家关于内置应用收费以及申请测试账号进行测试购买的经验;

在应用内嵌入付费代码这一快Himi可以直接将代码分享给大家,所以我们来说一些主要流程,毕竟没有接触过这一块的童鞋肯定相当头疼 =。  =

OK,步入整体,如果你想在iOS里内嵌收费,那么分为以下几步:

 

 【提示:以下创建App部分内容,你不用非要等项目能打包了才开始做,可以随时并且随便的创建个测试项目即可,因为嵌入付费并不要求上传App的ipa包的!!】   

 

第一步:你需要在iTunesConnect中创建个新的App,然后为这个App设置一些产品(付费道具)等;

OK,这里Himi稍微解释下,iTunesConnect是苹果提供的一个平台,主要提供AP发布和管理App的,最重要的功能是创建管理项目信息,项目付费产品(道具)管理、付费的测试账号、提交App等等,这里就简单介绍这么多,关于产品一词在此我们可以理解成游戏道具即可;在苹果看来所有付费都属于产品 =。 =千万不要纠结字眼哦~

OK,打开iTunesConnect网站:https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa (注意:企业级的用户必须使用公司主开发者账号登陆才可!)

成功登陆后的页面如下:

这里大概说下重要的一些项:

 Contracts, Tax, and Banking   : 管理银行账号、联系人以及税等等;这里要根据提示完成对应的信息填写!一定要详细填写喔~

             Manage Users :管理用户的,比如主账号以及测试付费的(测试App)账号;

             Manage Your Applictions:管理应用程序的,你所有发布的应用和每个应用的状态都在这里面;

下面我们新建一个App项目,大家放心,我们这里创建的是不会直接提交给App审核的,所以放心创建,只要控制好App的状态不要是待审核状态即可,不过即使你不小心将项目提交了,也没事,直接更改App状态即可了;

选择Manage Your Applictions选项,然后新建一个项目:【Add New App】,根据提示来填写吧,这里就不细致说明了~

创建好一个App之后,在点击Manage Your Applictions后的界面应该如下:

这里你将看到自己创建的App,点击你创建的App项目,这里Himi创建的项目名字叫”ProjectForBuyTest“,点击你的App进入如下界面:

 

  (注意:这里的Bundle ID一定要跟你的项目中的info.plist中的Bundle ID保证一致!!!!)

这里可以管理你的项目的信息、状态、是否嵌入GameCenter等等选项,那么本章我们重点介绍如何使用IAp沙盒测试程序内付费,所以这里我们点击右上角的”Manage In-App Purchases“选项进入创建产品(游戏道具)界面如下:

上图中的下方看到Himi创建过的四个产品(道具)了,你可以点击”Create New“选项新建一个产品(付费道具),点击新建如下界面:

 

上图中Himi没有截图出所有的选项,这里大概介绍下,这个界面是选择你的消费道具的种类,种类说明如下:

   类型选择有四种选择:

   1.Consumable(消耗品): 每次下载都需要付费;

   2.Non-consumable(非消耗品): 仅需付费一次;

   3.Auto-Renewable Subscriptions:自动订阅;

   4.Free Subscription:免费订阅

   最下方是你沙盒测试的截图,暂且不管即可;

   这里Himi选择Consumable选项,比如很多游戏都是购买金币啦这样子就可以选择这个;然后出现如下界面:

 

Reference Name: 付费产品(道具的)参考名称

   Product ID(产品ID): 你产品的唯一id。通常格式是 com.xx.yy,但它可以是任何形式,不要求以程序的App ID作为前缀。

   Add Language: 添加产品名称与描述语言;

   Price Tier:选择价格,这里你选择价格后,会出现如上图最下方的价格对照表

   Screenshot(截屏): 展示你产品的截屏。(这个直接无视,测试App务必要管这个的)

Product ID(产品ID)可以创建多个,比如我想游戏中分为0.99$ 、1.99$等道具那就创建对应多个产品ID

  我们填写好了”Reference Name“与”Product ID“以及”Price Tier“后,点击”Add Language“选项然后出现如下界面:

上图中的选项:

Language:语言

      Displayed Name(显示名称): 用户看到的产品名称。

      Description(描述): 对产品进行描述。

 

Ok,一路 Save保存回到”Manage In-App Purchases“界面中会看到我们新建的产品(道具)如下:

大家可以看到新建的产品(道具)ID:这里Himi创建的产品ID是com.himi.wahaha ,这里要记住这个产品ID哦~

 

第二步:申请测试账号,利用沙盒测试模拟AppStore购买道具流程!

  回到itunesconnect主页中,选择“Manage Users”然后选择“Test User”,然后出现的界面如下图:

这里Himi已经创建了两个测试账号了,点击界面中的 “Add New User”进行创建即可;记住账号和密码哈,记不住就删掉重新建 娃哈哈~(切记:不能用于真正的AppStore中使用此账号,不仅不能用,而且一旦AppStore发现后果你懂得~)

第三步:在项目中申请购买产品代码以及监听;

这里关于购买的代码部分呢,我都有备注的,Himi这里就不详细讲解了,Himi只是在代码后介绍几点值得注意的地方:

 

这里Himi是新建的一个Cocos2d的项目,然后给出HelloWorldLayer.h以及HelloWorldLayer.m的全部代码,所有购买代码也全在里面也对应有Himi的注释!

     HelloWorldLayer.h

 

  1. //
  2. //  HelloWorldLayer.h
  3. //  buytest
  4. //
  5. //  Created by 华明 李 on 11-10-29.
  6. //  Copyright Himi 2011年. All rights reserved.
  7. //
  8. // When you import this file, you import all the cocos2d classes
  9. #import “cocos2d.h”
  10. #import <UIKit/UIKit.h>
  11. #import <StoreKit/StoreKit.h>
  12. enum{
  13.      IAP0p99=10,
  14.      IAP1p99,
  15.      IAP4p99,
  16.      IAP9p99,
  17.      IAP24p99,
  18. }buyCoinsTag;
  19. @interface HelloWorldLayer : CCLayer<SKProductsRequestDelegate,SKPaymentTransactionObserver>
  20. {
  21.     int buyType;
  22. }
  23. +(CCScene *) scene;
  24. – (void) requestProUpgradeProductData;
  25. -(void)RequestProductData;
  26. -(bool)CanMakePay;
  27. -(void)buy:(int)type;
  28. – (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;
  29. -(void) PurchasedTransaction: (SKPaymentTransaction *)transaction;
  30. – (void) completeTransaction: (SKPaymentTransaction *)transaction;
  31. – (void) failedTransaction: (SKPaymentTransaction *)transaction;
  32. -(void) paymentQueueRestoreCompletedTransactionsFinished: (SKPaymentTransaction *)transaction;
  33. -(void) paymentQueue:(SKPaymentQueue *) paymentQueue restoreCompletedTransactionsFailedWithError:(NSError *)error;
  34. – (void) restoreTransaction: (SKPaymentTransaction *)transaction;
  35. -(void)provideContent:(NSString *)product;
  36. -(void)recordTransaction:(NSString *)product;
  37. @end

HelloWorldLayer.m

 

 

  1. //
  2. //  IapLayer.m
  3. //
  4. //  Created by Himi on 11-5-25.
  5. //  Copyright 2011年 李华明 . All rights reserved.
  6. //
  7. #import “HelloWorldLayer.h” 
  8. #define ProductID_IAP0p99 @”com.buytest.one”//$0.99  
  9. #define ProductID_IAP1p99 @”com.buytest.two” //$1.99 
  10. #define ProductID_IAP4p99 @”com.buytest.three” //$4.99  
  11. #define ProductID_IAP9p99 @”com.buytest.four” //$19.99  
  12. #define ProductID_IAP24p99 @”com.buytest.five” //$24.99  
  13. @implementation HelloWorldLayer
  14. +(CCScene *) scene
  15. {
  16.     CCScene *scene = [CCScene node];
  17.     HelloWorldLayer *layer = [HelloWorldLayer node];
  18.     [scene addChild: layer];
  19.     return scene;
  20. }
  21. -(id)init
  22. {
  23.     if ((self = [super init])) {
  24.         CGSize size = [[CCDirector sharedDirector] winSize];
  25.         CCSprite *iap_bg  = [CCSprite spriteWithFile:@“Icon.png”];
  26.         [iap_bg setPosition:ccp(size.width/2,size.height/2)];
  27.         [self addChild:iap_bg z:0];
  28.         //———————
  29.         //—-监听购买结果
  30.         [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
  31.         //申请购买
  32.         /*
  33.          enum{
  34.          IAP0p99=10,
  35.          IAP1p99,
  36.          IAP4p99,
  37.          IAP9p99,
  38.          IAP24p99,
  39.          }buyCoinsTag;
  40.          */
  41.         [self buy:IAP24p99];
  42.     }
  43.     return self;
  44. }
  45. -(void)buy:(int)type
  46. {
  47.     buyType = type;
  48.     if ([SKPaymentQueue canMakePayments]) {
  49.         //[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
  50.         [self RequestProductData];
  51.         CCLOG(@“允许程序内付费购买”);
  52.     }
  53.     else
  54.     {
  55.         CCLOG(@“不允许程序内付费购买”);
  56.         UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:@“Alert”
  57.                                                             message:@“You can‘t purchase in app store(Himi说你没允许应用程序内购买)”
  58.                                                            delegate:nil cancelButtonTitle:NSLocalizedString(@“Close(关闭)”,nil) otherButtonTitles:nil];
  59.         [alerView show];
  60.         [alerView release];
  61.     }
  62. }
  63. -(bool)CanMakePay
  64. {
  65.     return [SKPaymentQueue canMakePayments];
  66. }
  67. -(void)RequestProductData
  68. {
  69.     CCLOG(@“———请求对应的产品信息————“);
  70.     NSArray *product = nil;
  71.     switch (buyType) {
  72.         case IAP0p99:
  73.             product=[[NSArray alloc] initWithObjects:ProductID_IAP0p99,nil];
  74.             break;
  75.         case IAP1p99:
  76.             product=[[NSArray alloc] initWithObjects:ProductID_IAP1p99,nil];
  77.             break;
  78.         case IAP4p99:
  79.             product=[[NSArray alloc] initWithObjects:ProductID_IAP4p99,nil];
  80.             break;
  81.         case IAP9p99:
  82.             product=[[NSArray alloc] initWithObjects:ProductID_IAP9p99,nil];
  83.             break;
  84.         case IAP24p99:
  85.             product=[[NSArray alloc] initWithObjects:ProductID_IAP24p99,nil];
  86.             break;
  87.         default:
  88.             break;
  89.     }
  90.     NSSet *nsset = [NSSet setWithArray:product];
  91.     SKProductsRequest *request=[[SKProductsRequest alloc] initWithProductIdentifiers: nsset];
  92.     request.delegate=self;
  93.     [request start];
  94.     [product release];
  95. }
  96. //<SKProductsRequestDelegate> 请求协议
  97. //收到的产品信息
  98. – (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
  99.     NSLog(@“———–收到产品反馈信息————–“);
  100.     NSArray *myProduct = response.products;
  101.     NSLog(@“产品Product ID:%@”,response.invalidProductIdentifiers);
  102.     NSLog(@“产品付费数量: %d”, [myProduct count]);
  103.     // populate UI 
  104.     for(SKProduct *product in myProduct){
  105.         NSLog(@“product info”);
  106.         NSLog(@“SKProduct 描述信息%@”, [product description]);
  107.         NSLog(@“产品标题 %@” , product.localizedTitle);
  108.         NSLog(@“产品描述信息: %@” , product.localizedDescription);
  109.         NSLog(@“价格: %@” , product.price);
  110.         NSLog(@“Product id: %@” , product.productIdentifier);
  111.     }
  112.     SKPayment *payment = nil;
  113.     switch (buyType) {
  114.         case IAP0p99:
  115.             payment  = [SKPayment paymentWithProductIdentifier:ProductID_IAP0p99];    //支付$0.99
  116.             break;
  117.         case IAP1p99:
  118.             payment  = [SKPayment paymentWithProductIdentifier:ProductID_IAP1p99];    //支付$1.99
  119.             break;
  120.         case IAP4p99:
  121.             payment  = [SKPayment paymentWithProductIdentifier:ProductID_IAP4p99];    //支付$9.99
  122.             break;
  123.         case IAP9p99:
  124.             payment  = [SKPayment paymentWithProductIdentifier:ProductID_IAP9p99];    //支付$19.99
  125.             break;
  126.         case IAP24p99:
  127.             payment  = [SKPayment paymentWithProductIdentifier:ProductID_IAP24p99];    //支付$29.99
  128.             break;
  129.         default:
  130.             break;
  131.     }
  132.     CCLOG(@“———发送购买请求————“);
  133.     [[SKPaymentQueue defaultQueue] addPayment:payment];
  134.     [request autorelease];
  135. }
  136. – (void)requestProUpgradeProductData
  137. {
  138.     CCLOG(@“——请求升级数据———“);
  139.     NSSet *productIdentifiers = [NSSet setWithObject:@“com.productid”];
  140.     SKProductsRequest* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
  141.     productsRequest.delegate = self;
  142.     [productsRequest start];
  143. }
  144. //弹出错误信息
  145. – (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
  146.     CCLOG(@“——-弹出错误信息———-“);
  147.     UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:NSLocalizedString(@“Alert”,NULL) message:[error localizedDescription]
  148.                                                        delegate:nil cancelButtonTitle:NSLocalizedString(@“Close”,nil) otherButtonTitles:nil];
  149.     [alerView show];
  150.     [alerView release];
  151. }
  152. -(void) requestDidFinish:(SKRequest *)request
  153. {
  154.     NSLog(@“———-反馈信息结束————–“);
  155. }
  156. -(void) PurchasedTransaction: (SKPaymentTransaction *)transaction{
  157.     CCLOG(@“—–PurchasedTransaction—-“);
  158.     NSArray *transactions =[[NSArray alloc] initWithObjects:transaction, nil];
  159.     [self paymentQueue:[SKPaymentQueue defaultQueue] updatedTransactions:transactions];
  160.     [transactions release];
  161. }
  162. //<SKPaymentTransactionObserver> 千万不要忘记绑定,代码如下:
  163. //—-监听购买结果
  164. //[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
  165. – (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions//交易结果
  166. {
  167.     CCLOG(@“—–paymentQueue——–“);
  168.     for (SKPaymentTransaction *transaction in transactions)
  169.     {
  170.         switch (transaction.transactionState)
  171.         {
  172.             case SKPaymentTransactionStatePurchased://交易完成 
  173.                 [self completeTransaction:transaction];
  174.                 CCLOG(@“—–交易完成 ——–“);
  175.                 CCLOG(@“不允许程序内付费购买”);
  176.                 UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:@“Alert”
  177.                                                                     message:@“Himi说你购买成功啦~娃哈哈”
  178.                                                                    delegate:nil cancelButtonTitle:NSLocalizedString(@“Close(关闭)”,nil) otherButtonTitles:nil];
  179.                 [alerView show];
  180.                 [alerView release];
  181.                 break;
  182.             case SKPaymentTransactionStateFailed://交易失败 
  183.                 [self failedTransaction:transaction];
  184.                  CCLOG(@“—–交易失败 ——–“);
  185.                 UIAlertView *alerView2 =  [[UIAlertView alloc] initWithTitle:@“Alert”
  186.                                                                     message:@“Himi说你购买失败,请重新尝试购买~”
  187.                                                                    delegate:nil cancelButtonTitle:NSLocalizedString(@“Close(关闭)”,nil) otherButtonTitles:nil];
  188.                 [alerView2 show];
  189.                 [alerView2 release];
  190.                 break;
  191.             case SKPaymentTransactionStateRestored://已经购买过该商品 
  192.                 [self restoreTransaction:transaction];
  193.                  CCLOG(@“—–已经购买过该商品 ——–“);
  194.             case SKPaymentTransactionStatePurchasing:      //商品添加进列表
  195.                  CCLOG(@“—–商品添加进列表 ——–“);
  196.                 break;
  197.             default:
  198.                 break;
  199.         }
  200.     }
  201. }
  202. – (void) completeTransaction: (SKPaymentTransaction *)transaction
  203. {
  204.     CCLOG(@“—–completeTransaction——–“);
  205.     // Your application should implement these two methods.
  206.     NSString *product = transaction.payment.productIdentifier;
  207.     if ([product length] > 0) {
  208.         NSArray *tt = [product componentsSeparatedByString:@“.”];
  209.         NSString *bookid = [tt lastObject];
  210.         if ([bookid length] > 0) {
  211.             [self recordTransaction:bookid];
  212.             [self provideContent:bookid];
  213.         }
  214.     }
  215.     // Remove the transaction from the payment queue.
  216.     [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  217. }
  218. //记录交易
  219. -(void)recordTransaction:(NSString *)product{
  220.     CCLOG(@“—–记录交易——–“);
  221. }
  222. //处理下载内容
  223. -(void)provideContent:(NSString *)product{
  224.     CCLOG(@“—–下载——–“);
  225. }
  226. – (void) failedTransaction: (SKPaymentTransaction *)transaction{
  227.     NSLog(@“失败”);
  228.     if (transaction.error.code != SKErrorPaymentCancelled)
  229.     {
  230.     }
  231.     [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
  232. }
  233. -(void) paymentQueueRestoreCompletedTransactionsFinished: (SKPaymentTransaction *)transaction{
  234. }
  235. – (void) restoreTransaction: (SKPaymentTransaction *)transaction
  236. {
  237.     NSLog(@” 交易恢复处理”);
  238. }
  239. -(void) paymentQueue:(SKPaymentQueue *) paymentQueue restoreCompletedTransactionsFailedWithError:(NSError *)error{
  240.     CCLOG(@“——-paymentQueue—-“);
  241. }
  242. #pragma mark connection delegate
  243. – (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
  244. {
  245.     NSLog(@“%@”,  [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]);
  246. }
  247. – (void)connectionDidFinishLoading:(NSURLConnection *)connection{
  248. }
  249. – (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
  250.     switch([(NSHTTPURLResponse *)response statusCode]) {
  251.         case 200:
  252.         case 206:
  253.             break;
  254.         case 304:
  255.             break;
  256.         case 400:
  257.             break;
  258.         case 404:
  259.             break;
  260.         case 416:
  261.             break;
  262.         case 403:
  263.             break;
  264.         case 401:
  265.         case 500:
  266.             break;
  267.         default:
  268.             break;
  269.     }
  270. }
  271. – (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
  272.     NSLog(@“test”);
  273. }
  274. -(void)dealloc
  275. {
  276.     [super dealloc];
  277. }
  278. @end

 

 

代码注释的相当清楚了,没有什么可解释的,这里说几点值得注意的地方:

1.添加对应对应代码时不要忘记,添加框架 StoreKit.framework,如何添加框架请看我的博文【iOS-Cocos2d游戏开发之十四】音频/音效/视频播放(利用Cocos2D-iPhone-Extensions嵌入Cocos2d进行视频播放!)

2. 越狱机器无法沙盒测试!模拟器的话,Himi用4.3模拟器不可以,因为提示没有开启程序内付费- -(我都没看到模拟器有store的选项,so~);但是使用iOS5的模拟器可以测试沙盒,但是执行的顺序会有些问题,但是还没真机的童鞋可以使用,建议一切以真机实测为准

3. 千万不要忘记在iTunesConnect中创建App Bundle ID一定要跟你的项目中的info.plist中的Bundle ID保证一致!!!!

4. 以上代码中你需要修改的就是我在HelloWorldLayer.m类中的宏定义的Product ID(产品ID),例如Himi刚才新建了一个产品ID是“com.himi.wahaha”

然后我运行项目截图如下以及运行控制台打印的信息如下:

点击Buy之后运行截图以及打印信息:

输入测试账号密码后以及打印信息:

害羞这里Himi最后一张截图是没有购买成功,这里Himi是故意截图出来的,原因就是想告诉童鞋们:

 如果你的产品信息能够正常得到,但是始终无法成功的话,不要着急,因为你的产品要进入iTunes Connect,并且Apple准备好沙箱环境需要一些时间。Himi之前遇到过,然后在一天后我没有修改任何一行代码,但产品ID变为有效并能成功购买。=。 =郁闷ing~~  起始要使产品发布到Apple的网络系统是需要一段时间的!

最全面的常用正则表达式大全

很多不太懂正则的朋友,在遇到需要用正则校验数据时,往往是在网上去找很久,结果找来的还是不很符合要求。所以我最近把开发中常用的一些正则表达式整理了一下,在这里分享一下。给自己留个底,也给朋友们做个参考。

一、校验数字的表达式

 1 数字:^[0-9]*$
 2 n位的数字:^\d{n}$
 3 至少n位的数字:^\d{n,}$
 4 m-n位的数字:^\d{m,n}$
 5 零和非零开头的数字:^(0|[1-9][0-9]*)$
 6 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
 7 带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$
 8 正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
 9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
12 非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
13 非负整数:^\d+$ 或 ^[1-9]\d*|0$
14 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
15 非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
16 非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
17 正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
18 负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
19 浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$

二、校验字符的表达式

 1 汉字:^[\u4e00-\u9fa5]{0,}$
 2 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
 3 长度为3-20的所有字符:^.{3,20}$
 4 由26个英文字母组成的字符串:^[A-Za-z]+$
 5 由26个大写英文字母组成的字符串:^[A-Z]+$
 6 由26个小写英文字母组成的字符串:^[a-z]+$
 7 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
 8 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
 9 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+
12 禁止输入含有~的字符:[^~\x22]+

三、特殊需求表达式

 1 Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
 2 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
 3 InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
 4 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
 5 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$ 
 6 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
 7 身份证号(15位、18位数字):^\d{15}|\d{18}$
 8 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
 9 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
10 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
11 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$  
12 日期格式:^\d{4}-\d{1,2}-\d{1,2}
13 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
14 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$ 
15 钱的输入格式:
16    1.有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$ 
17    2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$ 
18    3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$ 
19    4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$ 
20    5.必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$ 
21    6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$ 
22    7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$ 
23    8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$ 
24    备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
25 xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
26 中文字符的正则表达式:[\u4e00-\u9fa5]
27 双字节字符:[^\x00-\xff]    (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
28 空白行的正则表达式:\n\s*\r    (可以用来删除空白行)
29 HTML标记的正则表达式:<(\S*?)[^>]*>.*?</\1>|<.*? />    (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)
30 首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$)    (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
31 腾讯QQ号:[1-9][0-9]{4,}    (腾讯QQ号从10000开始)
32 中国邮政编码:[1-9]\d{5}(?!\d)    (中国邮政编码为6位数字)
33 IP地址:\d+\.\d+\.\d+\.\d+    (提取IP地址时有用)
34 IP地址:((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))

mysql性能优化-慢查询分析、优化索引和配置

目录

一、优化概述

二、查询与索引优化分析

1性能瓶颈定位

Show命令

慢查询日志

explain分析查询

profiling分析查询

 

2索引及查询优化

三、配置优化

1)      max_connections

2)      back_log

3)      interactive_timeout

4)      key_buffer_size

5)      query_cache_size

6)      record_buffer_size

7)      read_rnd_buffer_size

8)      sort_buffer_size

9)      join_buffer_size

10)    table_cache

11)    max_heap_table_size

12)    tmp_table_size

13)    thread_cache_size

14)    thread_concurrency

15)    wait_timeout

 

一、 优化概述

MySQL数据库是常见的两个瓶颈是CPU和I/O的瓶颈,CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候。磁盘I/O瓶颈发生在装入数据远大于内存容量的时候,如果应用分布在网络上,那么查询量相当大的时候那么平瓶颈就会出现在网络上,我们可以用mpstat, iostat, sar和vmstat来查看系统的性能状态。

除了服务器硬件的性能瓶颈,对于MySQL系统本身,我们可以使用工具来优化数据库的性能,通常有三种:使用索引,使用EXPLAIN分析查询以及调整MySQL的内部配置。

二、查询与索引优化分析

在优化MySQL时,通常需要对数据库进行分析,常见的分析手段有慢查询日志,EXPLAIN 分析查询,profiling分析以及show命令查询系统状态及系统变量,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。

1 性能瓶颈定位Show命令

我们可以通过show命令查看MySQL状态及变量,找到系统的瓶颈:

Mysql> show status ——显示状态信息(扩展show status like ‘XXX’)

Mysql> show variables ——显示系统变量(扩展show variables like ‘XXX’)

Mysql> show innodb status ——显示InnoDB存储引擎的状态

Mysql> show processlist ——查看当前SQL执行,包括执行状态、是否锁表等

Shell> mysqladmin variables -u username -p password——显示系统变量

Shell> mysqladmin extended-status -u username -p password——显示状态信息

查看状态变量及帮助:

Shell> mysqld –verbose –help [|more #逐行显示]

 

比较全的Show命令的使用可参考: http://blog.phpbean.com/a.cn/18/

慢查询日志

慢查询日志开启:

在配置文件my.cnf或my.ini中在[mysqld]一行下面加入两个配置参数

log-slow-queries=/data/mysqldata/slow-query.log

long_query_time=2

注:log-slow-queries参数为慢查询日志存放的位置,一般这个目录要有mysql的运行帐号的可写权限,一般都将这个目录设置为mysql的数据存放目录;

long_query_time=2中的2表示查询超过两秒才记录;

在my.cnf或者my.ini中添加log-queries-not-using-indexes参数,表示记录下没有使用索引的查询。

log-slow-queries=/data/mysqldata/slow-query.log

long_query_time=10

log-queries-not-using-indexes

慢查询日志开启方法二:

我们可以通过命令行设置变量来即时启动慢日志查询。由下图可知慢日志没有打开,slow_launch_time=# 表示如果建立线程花费了比这个值更长的时间,slow_launch_threads 计数器将增加

设置慢日志开启

MySQL后可以查询long_query_time 的值 。

 

为了方便测试,可以将修改慢查询时间为5秒。

慢查询分析mysqldumpslow

我们可以通过打开log文件查看得知哪些SQL执行效率低下

[root@localhost mysql]# more slow-query.log

# Time: 081026 19:46:34

# User@Host: root[root] @ localhost []

# Query_time: 11 Lock_time: 0 Rows_sent: 1 Rows_examined: 6552961

select count(*) from t_user;

从日志中,可以发现查询时间超过5 秒的SQL,而小于5秒的没有出现在此日志中。

如果慢查询日志中记录内容很多,可以使用mysqldumpslow工具(MySQL客户端安装自带)来对慢查询日志进行分类汇总。mysqldumpslow对日志文件进行了分类汇总,显示汇总后摘要结果。

进入log的存放目录,运行

[root@mysql_data]#mysqldumpslow  slow-query.log

Reading mysql slow query log from slow-query.log

Count: 2 Time=11.00s (22s) Lock=0.00s (0s) Rows=1.0 (2), root[root]@mysql

select count(N) from t_user;

mysqldumpslow命令

/path/mysqldumpslow -s c -t 10 /database/mysql/slow-query.log

这会输出记录次数最多的10条SQL语句,其中:

-s, 是表示按照何种方式排序,c、t、l、r分别是按照记录次数、时间、查询时间、返回的记录数来排序,ac、at、al、ar,表示相应的倒叙;

-t, 是top n的意思,即为返回前面多少条的数据;

-g, 后边可以写一个正则匹配模式,大小写不敏感的;

例如:

/path/mysqldumpslow -s r -t 10 /database/mysql/slow-log

得到返回记录集最多的10个查询。

/path/mysqldumpslow -s t -t 10 -g “left join” /database/mysql/slow-log

得到按照时间排序的前10条里面含有左连接的查询语句。

使用mysqldumpslow命令可以非常明确的得到各种我们需要的查询语句,对MySQL查询语句的监控、分析、优化是MySQL优化非常重要的一步。开启慢查询日志后,由于日志记录操作,在一定程度上会占用CPU资源影响mysql的性能,但是可以阶段性开启来定位性能瓶颈。

explain分析查询

使用 EXPLAIN 关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。这可以帮你分析你的查询语句或是表结构的性能瓶颈。通过explain命令可以得到:

– 表的读取顺序

– 数据读取操作的操作类型

– 哪些索引可以使用

– 哪些索引被实际使用

– 表之间的引用

– 每张表有多少行被优化器查询

EXPLAIN字段:

ØTable:显示这一行的数据是关于哪张表的

Øpossible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从WHERE语句中选择一个合适的语句

Økey:实际使用的索引。如果为NULL,则没有使用索引。MYSQL很少会选择优化不足的索引,此时可以在SELECT语句中使用USE INDEX(index)来强制使用一个索引或者用IGNORE INDEX(index)来强制忽略索引

Økey_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好

Øref:显示索引的哪一列被使用了,如果可能的话,是一个常数

Ørows:MySQL认为必须检索的用来返回请求数据的行数

Øtype:这是最重要的字段之一,显示查询使用了何种类型。从最好到最差的连接类型为system、const、eq_reg、ref、range、index和ALL

nsystem、const:可以将查询的变量转为常量.  如id=1; id为 主键或唯一键.

neq_ref:访问索引,返回某单一行的数据.(通常在联接时出现,查询使用的索引为主键或惟一键)

nref:访问索引,返回某个值的数据.(可以返回多行) 通常使用=时发生

nrange:这个连接类型使用索引返回一个范围中的行,比如使用>或<查找东西,并且该字段上建有索引时发生的情况(注:不一定好于index)

nindex:以索引的顺序进行全表扫描,优点是不用排序,缺点是还要全表扫描

nALL:全表扫描,应该尽量避免

ØExtra:关于MYSQL如何解析查询的额外信息,主要有以下几种

nusing index:只用到索引,可以避免访问表.

nusing where:使用到where来过虑数据. 不是所有的where clause都要显示using where. 如以=方式访问索引.

nusing tmporary:用到临时表

nusing filesort:用到额外的排序. (当使用order by v1,而没用到索引时,就会使用额外的排序)

nrange checked for eache record(index map:N):没有好的索引.

 

profiling分析查询

通过慢日志查询可以知道哪些SQL语句执行效率低下,通过explain我们可以得知SQL语句的具体执行情况,索引使用等,还可以结合show命令查看执行状态。

如果觉得explain的信息不够详细,可以同通过profiling命令得到更准确的SQL执行消耗系统资源的信息。

profiling默认是关闭的。可以通过以下语句查看

 

打开功能: mysql>set profiling=1; 执行需要测试的sql 语句:

mysql> show profiles\G; 可以得到被执行的SQL语句的时间和ID

mysql>show profile for query 1; 得到对应SQL语句执行的详细信息

Show Profile命令格式:

SHOW PROFILE [type [, type] … ]

[FOR QUERY n]

[LIMIT row_count [OFFSET offset]]

type:

ALL

| BLOCK IO

| CONTEXT SWITCHES

| CPU

| IPC

| MEMORY

| PAGE FAULTS

| SOURCE

| SWAPS

 

 

 

 

以上的16rows是针对非常简单的select语句的资源信息,对于较复杂的SQL语句,会有更多的行和字段,比如converting HEAP to MyISAM 、Copying to tmp table等等,由于以上的SQL语句不存在复杂的表操作,所以未显示这些字段。通过profiling资源耗费信息,我们可以采取针对性的优化措施。

 

测试完毕以后 ,关闭参数:mysql> set profiling=0

 

 

2     索引及查询优化

 

索引的类型

Ø 普通索引:这是最基本的索引类型,没唯一性之类的限制。

Ø 唯一性索引:和普通索引基本相同,但所有的索引列值保持唯一性。

Ø 主键:主键是一种唯一索引,但必须指定为”PRIMARY KEY”。

Ø 全文索引:MYSQL从3.23.23开始支持全文索引和全文检索。在MYSQL中,全文索引的索引类型为FULLTEXT。全文索引可以在VARCHAR或者TEXT类型的列上创建。

大多数MySQL索引(PRIMARY KEY、UNIQUE、INDEX和FULLTEXT)使用B树中存储。空间列类型的索引使用R-树,MEMORY表支持hash索引。

单列索引和多列索引(复合索引)

索引可以是单列索引,也可以是多列索引。对相关的列使用索引是提高SELECT操作性能的最佳途径之一。

多列索引:

MySQL可以为多个列创建索引。一个索引可以包括15个列。对于某些列类型,可以索引列的左前缀,列的顺序非常重要。

多列索引可以视为包含通过连接索引列的值而创建的值的排序的数组。一般来说,即使是限制最严格的单列索引,它的限制能力也远远低于多列索引。

最左前缀

多列索引有一个特点,即最左前缀(Leftmost Prefixing)。假如有一个多列索引为key(firstname lastname age),当搜索条件是以下各种列的组合和顺序时,MySQL将使用该多列索引:

firstname,lastname,age

firstname,lastname

firstname

也就是说,相当于还建立了key(firstname lastname)和key(firstname)。

索引主要用于下面的操作:

Ø 快速找出匹配一个WHERE子句的行。

Ø 删除行。当执行联接时,从其它表检索行。

Ø 对具体有索引的列key_col找出MAX()或MIN()值。由预处理器进行优化,检查是否对索引中在key_col之前发生所有关键字元素使用了WHERE key_part_# = constant。在这种情况下,MySQL为每个MIN()或MAX()表达式执行一次关键字查找,并用常数替换它。如果所有表达式替换为常量,查询立即返回。例如:

SELECT MIN(key2), MAX (key2)  FROM tb WHERE key1=10;

Ø 如果对一个可用关键字的最左面的前缀进行了排序或分组(例如,ORDER BY key_part_1,key_part_2),排序或分组一个表。如果所有关键字元素后面有DESC,关键字以倒序被读取。

Ø 在一些情况中,可以对一个查询进行优化以便不用查询数据行即可以检索值。如果查询只使用来自某个表的数字型并且构成某些关键字的最左面前缀的列,为了更快,可以从索引树检索出值。

SELECT key_part3 FROM tb WHERE key_part1=1

有时MySQL不使用索引,即使有可用的索引。一种情形是当优化器估计到使用索引将需要MySQL访问表中的大部分行时。(在这种情况下,表扫描可能会更快些)。然而,如果此类查询使用LIMIT只搜索部分行,MySQL则使用索引,因为它可以更快地找到几行并在结果中返回。例如:

 

合理的建立索引的建议:

(1)  越小的数据类型通常更好:越小的数据类型通常在磁盘、内存和CPU缓存中都需要更少的空间,处理起来更快。

(2)  简单的数据类型更好:整型数据比起字符,处理开销更小,因为字符串的比较更复杂。在MySQL中,应该用内置的日期和时间数据类型,而不是用字符串来存储时间;以及用整型数据类型存储IP地址。

(3)  尽量避免NULL:应该指定列为NOT NULL,除非你想存储NULL。在MySQL中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值

 

这部分是关于索引和写SQL语句时应当注意的一些琐碎建议和注意点。

1. 当结果集只有一行数据时使用LIMIT 1

2. 避免SELECT *,始终指定你需要的列

从表中读取越多的数据,查询会变得更慢。他增加了磁盘需要操作的时间,还是在数据库服务器与WEB服务器是独立分开的情况下。你将会经历非常漫长的网络延迟,仅仅是因为数据不必要的在服务器之间传输。

3. 使用连接(JOIN)来代替子查询(Sub-Queries)

连接(JOIN).. 之所以更有效率一些,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。

4. 使用ENUMCHAR 而不是VARCHAR,使用合理的字段属性长度

5. 尽可能的使用NOT NULL

6. 固定长度的表会更快

7. 拆分大的DELETE INSERT 语句

8. 查询的列越小越快

 

Where条件

在查询中,WHERE条件也是一个比较重要的因素,尽量少并且是合理的where条件是很重要的,尽量在多个条件的时候,把会提取尽量少数据量的条件放在前面,减少后一个where条件的查询时间。

有些where条件会导致索引无效:

Ø where子句的查询条件里有!=,MySQL将无法使用索引。

Ø where子句使用了Mysql函数的时候,索引将无效,比如:select * from tb where left(name, 4) = ‘xxx’

Ø 使用LIKE进行搜索匹配的时候,这样索引是有效的:select * from tbl1 where name like ‘xxx%’,而like ‘%xxx%’ 时索引无效

 

三、    配置优化

安装MySQL后,配置文件my.cnf在 /MySQL安装目录/share/mysql目录中,该目录中还包含多个配置文件可供参考,有my-large.cnf ,my-huge.cnf,  my-medium.cnf,my-small.cnf,分别对应大中小型数据库应用的配置。win环境下即存在于MySQL安装目录中的.ini文件。

 

下面列出了对性能优化影响较大的主要变量,主要分为连接请求的变量和缓冲区变量。

1.   连接请求的变量:

1)     max_connections

MySQL的最大连接数,增加该值增加mysqld 要求的文件描述符的数量。如果服务器的并发连接请求量比较大,建议调高此值,以增加并行连接数量,当然这建立在机器能支撑的情况下,因为如果连接数越多,介于MySQL会为每个连接提供连接缓冲区,就会开销越多的内存,所以要适当调整该值,不能盲目提高设值。

数值过小会经常出现ERROR 1040: Too many connections错误,可以过’conn%’通配符查看当前状态的连接数量,以定夺该值的大小。

show variables like ‘max_connections’ 最大连接数

show  status like ‘max_used_connections’响应的连接数

如下:

mysql> show variables like ‘max_connections‘;

+———————–+——-+

| Variable_name | Value |

+———————–+——-+

| max_connections | 256  |

+———————–+——-+

mysql> show status like ‘max%connections‘;

+———————–+——-+

| Variable_name       | Value |

+—————————-+——-+

| max_used_connections | 256|

+—————————-+——-+

max_used_connections / max_connections * 100% (理想值≈ 85%)

如果max_used_connections跟max_connections相同 那么就是max_connections设置过低或者超过服务器负载上限了,低于10%则设置过大。

2)     back_log

MySQL能暂存的连接数量。当主要MySQL线程在一个很短时间内得到非常多的连接请求,这就起作用。如果MySQL的连接数据达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源。

back_log值指出在MySQL暂时停止回答新请求之前的短时间内有多少个请求可以被存在堆栈中。只有如果期望在一个短时间内有很多连接,你需要增加它,换句话说,这值对到来的TCP/IP连接的侦听队列的大小。

当观察你主机进程列表(mysql> show full processlist),发现大量264084 | unauthenticated user | xxx.xxx.xxx.xxx | NULL | Connect | NULL | login | NULL 的待连接进程时,就要加大back_log 的值了。

默认数值是50,可调优为128,对于Linux系统设置范围为小于512的整数。

3)     interactive_timeout

一个交互连接在被服务器在关闭前等待行动的秒数。一个交互的客户被定义为对mysql_real_connect()使用CLIENT_INTERACTIVE 选项的客户。

默认数值是28800,可调优为7200。

2.   缓冲区变量

全局缓冲:

4)     key_buffer_size

key_buffer_size指定索引缓冲区的大小,它决定索引处理的速度,尤其是索引读的速度。通过检查状态值Key_read_requests和Key_reads,可以知道key_buffer_size设置是否合理。比例key_reads / key_read_requests应该尽可能的低,至少是1:100,1:1000更好(上述状态值可以使用SHOW STATUS LIKE ‘key_read%’获得)。

key_buffer_size只对MyISAM表起作用。即使你不使用MyISAM表,但是内部的临时磁盘表是MyISAM表,也要使用该值。可以使用检查状态值created_tmp_disk_tables得知详情。

举例如下:

mysql> show variables like ‘key_buffer_size‘;

+——————-+————+

| Variable_name | Value      |

+———————+————+

| key_buffer_size | 536870912 |

+———— ———-+————+

key_buffer_size为512MB,我们再看一下key_buffer_size的使用情况:

mysql> show global status like ‘key_read%‘;

+————————+————-+

| Variable_name   | Value    |

+————————+————-+

| Key_read_requests| 27813678764 |

| Key_reads   |  6798830      |

+————————+————-+

一共有27813678764个索引读取请求,有6798830个请求在内存中没有找到直接从硬盘读取索引,计算索引未命中缓存的概率:

key_cache_miss_rate =Key_reads / Key_read_requests * 100%,设置在1/1000左右较好

默认配置数值是8388600(8M),主机有4GB内存,可以调优值为268435456(256MB)。

5)     query_cache_size

使用查询缓冲,MySQL将查询结果存放在缓冲区中,今后对于同样的SELECT语句(区分大小写),将直接从缓冲区中读取结果。

通过检查状态值Qcache_*,可以知道query_cache_size设置是否合理(上述状态值可以使用SHOW STATUS LIKE ‘Qcache%’获得)。如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,如果Qcache_hits的值也非常大,则表明查询缓冲使用非常频繁,此时需要增加缓冲大小;如果Qcache_hits的值不大,则表明你的查询重复率很低,这种情况下使用查询缓冲反而会影响效率,那么可以考虑不用查询缓冲。此外,在SELECT语句中加入SQL_NO_CACHE可以明确表示不使用查询缓冲。

 

与查询缓冲有关的参数还有query_cache_type、query_cache_limit、query_cache_min_res_unit。

 

query_cache_type指定是否使用查询缓冲,可以设置为0、1、2,该变量是SESSION级的变量。

query_cache_limit指定单个查询能够使用的缓冲区大小,缺省为1M。

query_cache_min_res_unit是在4.1版本以后引入的,它指定分配缓冲区空间的最小单位,缺省为4K。检查状态值Qcache_free_blocks,如果该值非常大,则表明缓冲区中碎片很多,这就表明查询结果都比较小,此时需要减小query_cache_min_res_unit。

举例如下:

mysql> show global status like ‘qcache%‘;

+——————————-+—————–+

| Variable_name                  | Value        |

+——————————-+—————–+

| Qcache_free_blocks        | 22756       |

| Qcache_free_memory     | 76764704    |

| Qcache_hits           | 213028692 |

| Qcache_inserts         | 208894227   |

| Qcache_lowmem_prunes   | 4010916      |

| Qcache_not_cached | 13385031    |

| Qcache_queries_in_cache | 43560 |

| Qcache_total_blocks          | 111212      |

+——————————-+—————–+

mysql> show variables like ‘query_cache%‘;

+————————————–+————–+

| Variable_name            | Value      |

+————————————–+———–+

| query_cache_limit         | 2097152     |

| query_cache_min_res_unit      | 4096    |

| query_cache_size         | 203423744 |

| query_cache_type        | ON           |

| query_cache_wlock_invalidate | OFF   |

+————————————–+—————+

查询缓存碎片率= Qcache_free_blocks / Qcache_total_blocks * 100%

如果查询缓存碎片率超过20%,可以用FLUSH QUERY CACHE整理缓存碎片,或者试试减小query_cache_min_res_unit,如果你的查询都是小数据量的话。

查询缓存利用率= (query_cache_size – Qcache_free_memory) / query_cache_size * 100%

查询缓存利用率在25%以下的话说明query_cache_size设置的过大,可适当减小;查询缓存利用率在80%以上而且Qcache_lowmem_prunes > 50的话说明query_cache_size可能有点小,要不就是碎片太多。

查询缓存命中率= (Qcache_hits – Qcache_inserts) / Qcache_hits * 100%

示例服务器查询缓存碎片率=20.46%,查询缓存利用率=62.26%,查询缓存命中率=1.94%,命中率很差,可能写操作比较频繁吧,而且可能有些碎片。

每个连接的缓冲

6)    record_buffer_size

每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,你可能想要增加该值。

默认数值是131072(128K),可改为16773120 (16M)

7)     read_rnd_buffer_size

随机读缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySQL会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySQL会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。

一般可设置为16M

8)     sort_buffer_size

每个需要进行排序的线程分配该大小的一个缓冲区。增加这值加速ORDER BY或GROUP BY操作。

默认数值是2097144(2M),可改为16777208 (16M)。

9)     join_buffer_size

联合查询操作所能使用的缓冲区大小

record_buffer_size,read_rnd_buffer_size,sort_buffer_size,join_buffer_size为每个线程独占,也就是说,如果有100个线程连接,则占用为16M*100

10)  table_cache

表高速缓存的大小。每当MySQL访问一个表时,如果在表缓冲区中还有空间,该表就被打开并放入其中,这样可以更快地访问表内容。通过检查峰值时间的状态值Open_tablesOpened_tables,可以决定是否需要增加table_cache的值。如果你发现open_tables等于table_cache,并且opened_tables在不断增长,那么你就需要增加table_cache的值了(上述状态值可以使用SHOW STATUS LIKE ‘Open%tables’获得)。注意,不能盲目地把table_cache设置成很大的值。如果设置得太高,可能会造成文件描述符不足,从而造成性能不稳定或者连接失败。

1G内存机器,推荐值是128-256。内存在4GB左右的服务器该参数可设置为256M或384M。

11)  max_heap_table_size

用户可以创建的内存表(memory table)的大小。这个值用来计算内存表的最大行数值。这个变量支持动态改变,即set @max_heap_table_size=#

这个变量和tmp_table_size一起限制了内部内存表的大小。如果某个内部heap(堆积)表大小超过tmp_table_size,MySQL可以根据需要自动将内存中的heap表改为基于硬盘的MyISAM表。

12)  tmp_table_size

通过设置tmp_table_size选项来增加一张临时表的大小,例如做高级GROUP BY操作生成的临时表。如果调高该值,MySQL同时将增加heap表的大小,可达到提高联接查询速度的效果,建议尽量优化查询,要确保查询过程中生成的临时表在内存中,避免临时表过大导致生成基于硬盘的MyISAM表

mysql> show global status like ‘created_tmp%‘;

+——————————–+———+

| Variable_name             | Value |

+———————————-+———+

| Created_tmp_disk_tables | 21197  |

| Created_tmp_files   | 58  |

| Created_tmp_tables  | 1771587 |

+——————————–+———–+

每次创建临时表,Created_tmp_tables增加,如果临时表大小超过tmp_table_size,则是在磁盘上创建临时表,Created_tmp_disk_tables也增加,Created_tmp_files表示MySQL服务创建的临时文件文件数,比较理想的配置是:

Created_tmp_disk_tables / Created_tmp_tables * 100% <= 25%比如上面的服务器Created_tmp_disk_tables / Created_tmp_tables * 100% =1.20%,应该相当好了

默认为16M,可调到64-256最佳,线程独占,太大可能内存不够I/O堵塞

13)  thread_cache_size

可以复用的保存在中的线程的数量。如果有,新的线程从缓存中取得,当断开连接的时候如果有空间,客户的线置在缓存中。如果有很多新的线程,为了提高性能可以这个变量值。

通过比较 Connections和Threads_created状态的变量,可以看到这个变量的作用。

默认值为110,可调优为80。

14)  thread_concurrency

推荐设置为服务器 CPU核数的2倍,例如双核的CPU, 那么thread_concurrency的应该为4;2个双核的cpu, thread_concurrency的值应为8。默认为8

15)  wait_timeout

指定一个请求的最大连接时间,对于4GB左右内存的服务器可以设置为5-10。

 

3.    配置InnoDB的几个变量

innodb_buffer_pool_size

对于InnoDB表来说,innodb_buffer_pool_size的作用就相当于key_buffer_size对于MyISAM表的作用一样。InnoDB使用该参数指定大小的内存来缓冲数据和索引。对于单独的MySQL数据库服务器,最大可以把该值设置成物理内存的80%。

根据MySQL手册,对于2G内存的机器,推荐值是1G(50%)。

 

innodb_flush_log_at_trx_commit

主要控制了innodb将log buffer中的数据写入日志文件并flush磁盘的时间点,取值分别为0、1、2三个。0,表示当事务提交时,不做日志写入操作,而是每秒钟将log buffer中的数据写入日志文件并flush磁盘一次;1,则在每秒钟或是每次事物的提交都会引起日志文件写入、flush磁盘的操作,确保了事务的ACID;设置为2,每次事务提交引起写入日志文件的动作,但每秒钟完成一次flush磁盘操作。

实际测试发现,该值对插入数据的速度影响非常大,设置为2时插入10000条记录只需要2秒,设置为0时只需要1秒,而设置为1时则需要229秒。因此,MySQL手册也建议尽量将插入操作合并成一个事务,这样可以大幅提高速度。

根据MySQL手册,在允许丢失最近部分事务的危险的前提下,可以把该值设为0或2。

 

innodb_log_buffer_size

log缓存大小,一般为1-8M,默认为1M,对于较大的事务,可以增大缓存大小。

可设置为4M或8M。

 

innodb_additional_mem_pool_size

该参数指定InnoDB用来存储数据字典和其他内部数据结构的内存池大小。缺省值是1M。通常不用太大,只要够用就行,应该与表结构的复杂度有关系。如果不够用,MySQL会在错误日志中写入一条警告信息。

根据MySQL手册,对于2G内存的机器,推荐值是20M,可适当增加。

 

innodb_thread_concurrency=8

推荐设置为 2*(NumCPUs+NumDisks),默认一般为8

redis持久化和常见故障

redis持久化和常见故障 – SRE-网站可靠性工程师之路 – SegmentFault

redis 主从复制

Redis主从复制的原理

当建立主从关系时,slave配置slaveof <master_host> <master_port> 。slave服务器会向主服务器发送一个sync命令。master接受并fork一个进程来执行BGSAVE命令。该命令生成一个RDB文件并且全量发送给slave服务器,slave服务器接收并载入RDB文件,同时,主服务器将缓冲区的命令以增量的方式发送给从服务器,最终使从服务器的数据状态和主服务器保持一致。

RDB的工作原理

当redis生成dump.rdb文件时,工作过程如下

  • redis主进程fork一个子进程
  • fork出来的子进程将内存的数据集dump到临时的RDB中
  • 当子进程对临时的RDB文件写入完毕,redis用新的RDB文件代替旧的RDB文件

AOF的工作原理

AOF :append only file。每当Redis执行一个改变数据集的命令时,这个命令都会被追加到AOF文件的末尾。当redis重新启动时,程序可以通过AOF文件恢复数据

持久化文件监控

Redis 监控最直接的方法当然就是使用系统提供的 info 命令来做了,只需要执行下面一条命令,就能获得 Redis 系统的状态报告。

redis-cli info

RDB文件状态监控

其中跟RDB文件状态监控相关的参数

  • rdb_changes_since_last_save 表明上次RDB保存以后改变的key次数
  • rdb_bgsave_in_progress 表示当前是否在进行bgsave操作。是为1
  • rdb_last_save_time 上次保存RDB文件的时间戳
  • rdb_last_bgsave_time_sec 上次保存的耗时
  • rdb_last_bgsave_status 上次保存的状态
  • rdb_current_bgsave_time_sec 目前保存RDB文件已花费的时间

AOF文件状态监控

其中跟AOF文件状态监控相关的参数

  • aof_enabled AOF文件是否启用
  • aof_rewrite_in_progress 表示当前是否在进行写入AOF文件操作
  • aof_rewrite_scheduled
  • aof_last_rewrite_time_sec 上次写入的时间戳
  • aof_current_rewrite_time_sec:-1
  • aof_last_bgrewrite_status:ok 上次写入状态
  • aof_last_write_status:ok 上次写入状态

查看rdb文件生成耗时

在我们优化master之前,可以看看目前我们的其中一个生产环境的的redis的持久化状态

# Persistence
loading:0
rdb_changes_since_last_save:116200
rdb_bgsave_in_progress:1
rdb_last_save_time:1448944451
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:85
rdb_current_bgsave_time_sec:33
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok

通过redis-cli的info命令,可以看到 「rdb_last_bgsave_time_sec」参数的值,
这个值表示上次bgsave命令执行的时间。在磁盘IO定量的情况下,redis占用的内存越大,
这个值也就越大。通常「rdb_last_bgsave_time_sec」这个时间取决于两个因素:

  • REDIS占用的内存大小
  • 磁盘的写速度。

rdb_last_bgsave_time_sec:85 这个标识表示我们上次保存dump RDB文件的时间。这个耗时受限于上面提到的两个因素。

当redis处于 rdb_bgsave_in_progress状态时,通过vmstat命令查看性能,得到wa值偏高,也就是说CPU在等待
IO的请求完成,我们线上的一个应用redis占用的内存是5G左右,也就是redis会生成大约5G左右的dump.rdb文件

vmstat命令

  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  4      0 223912 2242680 5722008    0    0   200 48648 3640 5443  1  1 63 35  0
 0  3      0 222796 2242680 5722052    0    0    16 48272 2417 5019  1  1 63 35  0
 0  3      0 222300 2242680 5722092    0    0    40 24612 3042 3568  1  1 63 35  0
 0  3      0 220068 2242680 5722124    0    0    64 40328 4304 4737  2  1 63 34  0
 0  3      0 218952 2242680 5722216    0    0   100 48648 4966 5786  1  2 63 35  0
 0  3      0 215356 2242680 5722256    0    0     0 66168 3546 4382  2  1 62 35  0

通过上面的输出,看到BGSAVE 对于IO的性能影响比较大

那么该如何解决由RDB文件带来的性能上不足的问题,又能保证数据持久化的目的

通常的设计思路就是利用「Replication」机制来解决:即master不开启RDB日志和AOF日志,来保证master的读写性能。而slave则开启rdb和aof来进行持久化,保证数据的持久性,

建立主从复制步骤和灾难恢复

我在测试机器上,开启两个实例,端口分别为6379和6380

master: 172.16.76.232 6379
slave:  172.16.76.232 6380

修改配置

修改master的redis.conf

关闭RDB

# save 900 1
# save 300 10
# save 60 10000

关闭AOF

appendonly no

分别启动master和slave的redis

service redis start

修改slave配置,指向master服务器

redis-cli > slaveof 172.16.76.232 6379

查看slave的复制状态

redis-cli > info replication

脚本模拟填充数据


#!/bin/bash

ID=1
while(($ID<50001))
do
 redis-cli set "my:$ID" "aaa_okthisis_Omb5EVIwBgPHgbRj64raygpeRLKaNhyB9sLF_$ID"
 redis-cli set "your:$ID" "your_okthisis_Omb5EVIwBgPHgbRj64raygpeRLKaNhyB9sLF_$ID"
 redis-cli set "her:$ID" "her_okthisis_Omb5EVIwBgPHgbRj64raygpeRLKaNhyB9sLF_$ID"
 redis-cli set "his:$ID" "his_okthisis_Omb5EVIwBgPHgbRj64raygpeRLKaNhyB9sLF_$ID"

 ID=$(($ID+1))
done

kill掉master实例模拟灾难

master redis > killall -9 redis-server
SLAVE redis > SLAVEOF NO ONE

取消Slave的同步,避免主库在未完成数据恢复前就重启,进而直接覆盖掉从库上的数据,导致所有的数据丢失。

将slave上的RDB和AOF复制到master数据文件夹中

cp /data/redis_data_slave/dump.rdb /data/redis_data/
cp /data/redis_data_slave/Append.AOF /data/redis_data/

启动master的实例

master redis > dbsize

查看数据是否恢复

重新开启slave复制

slave redis > slaveof 172.16.76.232 6379

故障案例报告

redis丢失数据案例

背景介绍:

我们的一台redis服务器,硬件配置为4核,4G内存。redis持久话方案是RDB。前面几个月redis使用的

内存在1G左右。在一次重启之后,redis只恢复了部分数据,这时查看redis.log文件。看见了如下的错误

[23635] 25 Jul 08:30:54.059 * 10000 changes in 60 seconds. Saving...
[23635] 25 Jul 08:30:54.059 # Can't save in background: fork: Cannot allocate memory

这时,想起了redis启动时的警告

WARNING overcommit_memory is set to 0!
Background save may fail under low memory condition.
To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and
then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.

翻译

警告:过量使用内存设置为0!在低内存环境下,后台保存可能失败。为了修正这个问题,
请在/etc/sysctl.conf 添加一项 'vm.overcommit_memory = 1' ,
然后重启(或者运行命令'sysctl vm.overcommit_memory=1' )使其生效。

vm.overcommit_memory不同的值说明

  • 0 表示检查是否有足够的内存可用,如果是,允许分配;如果内存不够,拒绝该请求,并返回一个错误给应用程序。
  • 1 允许分配超出物理内存加上交换内存的请求
  • 2 内核总是返回true

redis的数据回写机制分为两种

  • 同步回写即SAVE命令。redis主进程直接写数据到磁盘。当数据量大时,这个命令将阻塞,响应时间长
  • 异步回写即BGSAVE命令。redis 主进程fork一个子进程,复制主进程的内存并通过子进程回写数据到磁盘。

由于RDB文件写的时候fork一个子进程。相当于复制了一个内存镜像。当时系统的内存是4G,而redis占用了
近3G的内存,因此肯定会报内存无法分配。如果 「vm.overcommit_memory」设置为0,在可用内存不足的情况
下,就无法分配新的内存。如果 「vm.overcommit_memory」设置为1。 那么redis将使用交换内存。

解决办法:

  • 方法一: 修改内核参数 vi /etc/sysctl。设置 vm.overcommit_memory = 1 然后执行
    ``` sysctl -p ```
  • 方法二: 使用交换内存并不是一个完美的方案。最好的办法是扩大物理内存。

复制有可能碰到的问题

使用slaveof命令后,长时间看不到数据同步。以为复制功能失效,或配置错了。其实,不用担心,有两种方法可以确定是否正在建立复制。

在创建Redis复制时,一开始可能会发现Slave长时间不开始同步数据,可能数据量太大,导致了Master正在dump数据慢,此时如果你在Master上执行「top -p $(pgrep -d, redis-server)」命令,就能看到dump的过程

方式一: 通过「top」命令

[root@img1_u ~]# top -p $(pgrep -d, redis-server)
top - 14:06:24 up 54 days,  6:13,  1 user,  load average: 1.18, 1.32, 1.20
Tasks:   2 total,   1 running,   1 sleeping,   0 stopped,   0 zombie
Cpu(s): 15.2%us,  1.7%sy,  0.6%ni, 81.9%id,  0.2%wa,  0.0%hi,  0.4%si,  0.0%st
Mem:  24542176k total, 22771848k used,  1770328k free,  2245720k buffers
Swap:   524280k total,        0k used,   524280k free,  4369452k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
21619 root      20   0 5654m 5.4g  388 R 99.9 23.0   0:23.70 redis-server
 1663 root      20   0 5654m 5.4g 1068 S 15.3 23.0   5042:31 redis-server

redis-server是单进程的,现在通过「top」命令查看已经有2个进程,因为之前提到的,redis在建立复制的时,会在

主服务器上执行 BGSAVE 命令。fork一个子进程,dump出RDB文件。 master dump 完毕,然后再将快照文件传给slave。

方式二:通过「rdb_bgsave_in_progress」标识

进入master的redis-cli

redis-cli > info persistence
...
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:1
rdb_last_save_time:1448992510
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:4
rdb_current_bgsave_time_sec:1
...

如果「rdb_bgsave_in_progress」为1,那么master正在进行bgsave命令。同时「rdb_current_bgsave_time_sec」
显示bgsave命令已经执行的时间。由于在master服务器上默认不开启RDB和AOF日志,如果「rdb_bgsave_in_progress」为1,那么就可以肯定由于复制原因发送一个「bgsave」指令 dump 出 RDB 文件。

redis 内存达到上限

有运营的同事反应,系统在登录的情况下,操作时会无缘无故跳到登录页面。 由于我们的系统做了分布式的
session,默认把session放到redis里,按照以往的故障经验。可能是redis使用了最大内存上限
导致了无法设置key。 登录 redis 服务器查看 redis.conf 文件设置了最大8G内存「maxmemory 8G」
然后通过「redis-cli info memory 」 查询到目前的内存使用情况 「used_memory_human:7.71G」
接着通过redis-cli工具设置值 。报错 「OOM command not allowed when used memory 」。再次
验证了redis服务器已经达到了最大内存

解决方法:

  1. 关闭redis 服务器 redis-cli shutdown
  2. 修改配置文件的最大内存 「maxmemory」
  3. 启动redis服务器 redis-server redis.conf

【高并发简单解决方案】redis队列缓存 + mysql 批量入库 + php离线整合

【高并发简单解决方案】redis队列缓存 + mysql 批量入库 + php离线整合 – 靠谱崔小拽 – SegmentFault

问题分析

思考:应用网站架构的衍化过程中,应用最新的框架和工具技术固然是最优选择;但是,如果能在现有的框架的基础上提出简单可依赖的解决方案,未尝不是一种提升自我的尝试。

解决:

  • 问题一:要求日志最好入库;但是,直接入库mysql确实扛不住,批量入库没有问题,done。【批量入库和直接入库性能差异参考文章
  • 问题二:批量入库就需要有高并发的消息队列,决定采用redis list 仿真实现,而且方便回滚。
  • 问题三:日志量毕竟大,保存最近30条足矣,决定用php写个离线统计和清理脚本。

done,下面是小拽的简单实现过程

一:设计数据库表和存储

  • 考虑到log系统对数据库的性能更多一些,稳定性和安全性没有那么高,存储引擎自然是只支持select insert 没有索引的archive。如果确实有update需求,也可以采用myISAM。
  • 考虑到log是实时记录的所有数据,数量可能巨大,主键采用bigint,自增即可
  • 考虑到log系统以写为主,统计采用离线计算,字段均不要出现索引,因为一方面可能会影响插入数据效率,另外读时候会造成死锁,影响写数据。

二:redis存储数据形成消息队列

由于高并发,尽可能简单,直接,上代码。

<?php
/***************************************************************************
*
* 获取到的调用日志,存入redis的队列中.
* $Id$
*
**************************************************************************/

/**
* @file saveLog.php
* @date 2015/11/06 20:47:13
* @author:cuihuan
* @version $Revision$
* @brief
*
**/

// 获取info
$interface_info = $_GET['info'];

// 存入redis队列
$redis = new Redis();
$redis->connect('xx', 6379);
$redis->auth("password");

// 加上时间戳存入队列
$now_time = date("Y-m-d H:i:s");
$redis->rPush("call_log", $interface_info . "%" . $now_time);
$redis->close();


/* vim: set ts=4 sw=4 sts=4 tw=100 */
?>

三:数据定时批量入库。

定时读取redis消息队列里面的数据,批量入库。

<?php
/**
 * 获取redis消息队列中的脚本,拼接sql,批量入库。
 * @update 2015-11-07 添加失败消息队列回滚机制 
 *
 * @Author:cuihuan
 * 2015-11-06
 * */

// init redis
$redis_xx = new Redis();
$redis_xx->connect('ip', port);
$redis_xx->auth("password");

// 获取现有消息队列的长度
$count = 0;
$max = $redis_xx->lLen("call_log");

// 获取消息队列的内容,拼接sql
$insert_sql = "insert into fb_call_log (`interface_name`, `createtime`) values ";

// 回滚数组
$roll_back_arr = array();

while ($count < $max) {
    $log_info = $redis_cq01->lPop("call_log");
    $roll_back_arr = $log_info;
    if ($log_info == 'nil' || !isset($log_info)) {
        $insert_sql .= ";";
        break;
    }

    // 切割出时间和info
    $log_info_arr = explode("%",$log_info);
    $insert_sql .= " ('".$log_info_arr[0]."','".$log_info_arr[1]."'),";
    $count++;
}

// 判定存在数据,批量入库
if ($count != 0) {
    $link_2004 = mysql_connect('ip:port', 'user', 'password');
    if (!$link_2004) {
        die("Could not connect:" . mysql_error());
    }

    $crowd_db = mysql_select_db('fb_log', $link_2004);
    $insert_sql = rtrim($insert_sql,",").";";
    $res = mysql_query($insert_sql);

    // 输出入库log和入库结果;
    echo date("Y-m-d H:i:s")."insert ".$count." log info result:";
    echo json_encode($res);
    echo "</br>\n";
    
    // 数据库插入失败回滚
    if(!$res){
       foreach($roll_back_arr as $k){
           $redis_xx->rPush("call_log", $k);
       }
    }
 
    // 释放连接
    mysql_free_result($res);
    mysql_close($link_2004);
}

// 释放redis
$redis_cq01->close();
?>

四:离线天级统计和清理数据脚本

?php
/**
* static log :每天离线统计代码日志和删除五天前的日志
*
* @Author:cuihuan@baidu.com
* 2015-11-06
* */

// 离线统计
$link_2004 = mysql_connect('ip:port', 'user', 'pwd');
if (!$link_2004) {
    die("Could not connect:" . mysql_error());
}

$crowd_db = mysql_select_db('fb_log', $link_2004);

// 统计昨天的数据
$day_time = date("Y-m-d", time() - 60 * 60 * 24 * 1);
$static_sql = "get sql";

$res = mysql_query($static_sql, $link_2004);

// 获取结果入库略

// 清理15天之前的数据
$before_15_day = date("Y-m-d", time() - 60 * 60 * 24 * 15);
$delete_sql = "delete from xxx where createtime < '" . $before_15_day . "'";
try {
    $res = mysql_query($delete_sql);
}catch(Exception $e){
    echo json_encode($e)."\n";
    echo "delete result:".json_encode($res)."\n";
}

mysql_close($link_2004);
?>

五:代码部署

主要是部署,批量入库脚本的调用和天级统计脚本,crontab例行运行。

# 批量入库脚本
*/2 * * * * /home/cuihuan/xxx/lamp/php5/bin/php /home/cuihuan/xxx/batchLog.php >>/home/cuihuan/xxx/batchlog.log

# 天级统计脚本
0 5 * * * /home/cuihuan/xxx/php5/bin/php /home/cuihuan/xxx/staticLog.php >>/home/cuihuan/xxx/staticLog.log

总结:相对于其他复杂的方式处理高并发,这个解决方案简单有效:通过redis缓存抗压,mysql批量入库解决数据库瓶颈,离线计算解决统计数据,通过定期清理保证库的大小。

【转载请注明:高并发简单解决方案 | 靠谱崔小拽

性能优化模式

摘要

性能优化涉及面很广。一般而言,性能优化指降低响应时间和提高系统吞吐量两个方面,但在流量高峰时候,性能问题往往会表现为服务可用性下降,所以性能优化也可以包括提高服务可用性。在某些情况下,降低响应时间、提高系统吞吐量和提高服务可用性三者相互矛盾,不可兼得。例如:增加缓存可以降低平均响应时间,但是处理线程数量会因为缓存过大而有所限制,从而降低系统吞吐量;为了提高服务可用性,对异常请求重复调用是一个常用的做法,但是这会提高响应时间并降低系统吞吐量。

对于很多像美团这样的公司,它们的系统会面临如下三个挑战:1. 日益增长的用户数量,2. 日渐复杂的业务,3. 急剧膨胀的数据。这些挑战对于性能优化而言表现为:在保持和降低系统TP95响应时间(指的是将一段时间内的请求响应时间从低到高排序,高于95%请求响应时间的下确界)的前提下,不断提高系统吞吐量,提升流量高峰时期的服务可用性。这种场景下,三者的目标和改进方法取得了比较好的一致。本文主要目标是为类似的场景提供优化方案,确保系统在流量高峰时期的快速响应和高可用。

文章第一部分是介绍,包括采用模式方式讲解的优点,文章所采用案例的说明,以及后面部分用到的一些设计原则;第二部分介绍几种典型的“性能恶化模式”,阐述导致系统性能恶化,服务可用性降低的典型场景以及形成恶化循环的过程;第三部分是文章重点,阐述典型的“性能优化模式”,这些模式或者可以使服务远离“恶化模式”,或者直接对服务性能进行优化;文章最后一部分进行总结,并对未来可能出现的新模式进行展望。


介绍

模式讲解方式

关于性能优化的文章和图书已有很多,但就我所知,还没有采用模式的方式去讲解的。本文借鉴《设计模式》(“Design Patterns-Elements of Reusable Object-Oriented Software”)对设计模式的阐述方式,首先为每一种性能优化模式取一个贴切的名字,便于读者快速理解和深刻记忆,接着讲解该模式的动机和原理,然后结合作者在美团的具体工作案例进行深度剖析,最后总结采用该模式的优点以及需要付出的代价。简而言之,本文采用“命名–>原理和动机–>具体案例–>缺点和优点”的四阶段方式进行性能优化模式讲解。与其他方式相比,采用模式进行讲解有两个方面的优点:一方面,读者不仅仅能够掌握优化手段,而且能够了解采用该手段进行性能优化的场景以及所需付出的代价,这有利于读者全面理解和灵活应用;另一方面,模式解决的是特定应用场景下的一类问题,所以应用场景描述贯穿于模式讲解之中。如此,即使读者对原理不太了解,只要碰到的问题符合某个特定模式的应用场景(这往往比理解原理要简单),就可以采用对应的手段进行优化,进一步促进读者对模式的理解和掌握。

案例说明

文章的所有案例都来自于美团的真实项目。出于两方面的考虑,作者做了一定的简化和抽象:一方面,系统可以优化的问题众多,而一个特定的模式只能解决几类问题,所以在案例分析过程中会突出与模式相关的问题;另一方面,任何一类问题都需要多维度数据去描述,而应用性能优化模式的前提是多维度数据的组合值超过了某个临界点,但是精确定义每个维度数值的临界点是一件很难的事情,更别说多维度数据组合之后临界点。因此有必要对案例做一些简化,确保相关取值范围得到满足。基于以上以及其他原因,作者所给出的解决方案只是可行性方案,并不保证其是所碰到问题的最佳解决方案。

案例涉及的所有项目都是基于Java语言开发的,严格地讲,所有模式适用的场景是基于Java语言搭建的服务。从另外一方面讲,Java和C++的主要区别在于垃圾回收机制,所以,除去和垃圾回收机制紧密相关的模式之外,文章所描述的模式也适用于采用C++语言搭建的服务。对于基于其他语言开发的服务,读者在阅读以及实践的过程中需要考虑语言之间的差别。

设计原则

必须说明,本文中各种模式所要解决的问题之所以会出现,部分是因为工程师运用了某些深层次的设计原则。有些设计原则看上去和优秀的设计理念相悖,模式所解决的问题似乎完全可以避免,但是它们却被广泛使用。“存在即合理”,世界上没有完美的设计方案,任何方案都是一系列设计原则的妥协结果,所以本文主要关注点是解决所碰到的问题而不是如何绕过这些设计原则。下面对文中重要的设计原则进行详细阐述,在后面需要运用该原则时将不再解释。

最小可用原则

最小可用原则(快速接入原则)有两个关注点:1. 强调快速接入,快速完成;2. 实现核心功能可用。这是一个被普遍运用的原则,其目标是缩短测试周期,增加试错机会,避免过度设计。为了快速接入就必须最大限度地利用已有的解决方案或系统。从另外一个角度讲,一个解决方案或系统只要能够满足基本需求,就满足最小可用原则的应用需求。过度强调快速接入原则会导致重构风险的增加,原则上讲,基于该原则去设计系统需要为重构做好准备。

经济原则

经济原则关注的是成本问题,看起来很像最小可用原则,但是它们之间关注点不同。最小可用原则的目标是通过降低开发周期,快速接入而实现风险可控,而快速接入并不意味着成本降低,有时候为了实现快速接入可能需要付出巨大的成本。软件项目的生命周期包括:预研、设计、开发、测试、运行、维护等阶段。最小可用原则主要运用在预研阶段,而经济原则可以运用在整个软件生命周期里,也可以只关注某一个或者几个阶段。例如:运行时经济原则需要考虑的系统成本包括单次请求的CPU、内存、网络、磁盘消耗等;设计阶段的经济原则要求避免过度设计;开发阶段的经济原则可能关注代码复用,工程师资源复用等。

代码复用原则

代码复用原则分为两个层次:第一个层次使用已有的解决方案或调用已存在的共享库(Shared Library),也称为方案复用;第二个层次是直接在现有的代码库中开发,也称之为共用代码库。

方案复用是一个非常实用主义的原则,它的出发点就是最大限度地利用手头已有的解决方案,即使这个方案并不好。方案的形式可以是共享库,也可以是已存在的服务。方案复用的例子参见避免蚊子大炮模式的具体案例。用搜索引擎服务来解决查找附近商家的问题是一个性能很差的方案,但仍被很多工程师使用。方案复用原则的一个显著优点就是提高生产效率,例如:Java之所以能够得到如此广泛应用,原因之一就是有大量可以重复利用的开源库。实际上“Write once, run anywhere”是Java语言最核心的设计理念之一。基于Java语言开发的代码库因此得以在不同硬件平台、不同操作系统上更广泛地使用。

共用代码库要求在同一套代码库中完成所有功能开发。采用这个原则,代码库中的所有功能编译时可见,新功能代码可以无边界的调用老代码。另外,原代码库已存在的各种运行、编译、测试、配置环境可复用。主要有两个方面地好处:1. 充分利用代码库中已有的基础设施,快速接入新业务;2. 直接调用原代码中的基础功能或原語,避免网络或进程间调用开销,性能更佳。共用代码库的例子参见垂直分割模式的具体案例。

从设计的角度上讲,方案复用类似于微服务架构(Microservice Architecture,有些观点认为这是一种形式的SOA),而共用代码库和Monolithic Architecture很接近。总的来说,微服务倾向于面向接口编程,要求设计出可重用性的组件(Library或Service),通过分层组织各层组件来实现良好的架构。与之相对应,Monolith Architecture则希望尽可能在一套代码库中开发,通过直接调用代码中的基础功能或原語而实现性能的优化和快速迭代。使用Monolith Architecture有很大的争议,被认为不符合“设计模式”的理念。参考文献[4],Monolithic Design主要的缺点包括:1. 缺乏美感;2. 很难重构;3. 过早优化(参见文献[6]Optimize judiciously); 4. 不可重用;5. 限制眼界。微服务架构是很多互联网公司的主流架构,典型的运用公司包括Amazon、美团等。Monolithic Architecture也有其忠实的粉丝,例如:Tripadvisor的全球网站就共用一套代码库;基于性能的考虑,Linux最终选择的也是Monolithic kernel的模式。

奥卡姆剃刀原则

系统设计以及代码编写要遵循奥卡姆剃刀原则:Entities should not be multiplied unnecessarily。一般而言,一个系统的代码量会随着其功能增加而变多。系统的健壮性有时候也需要通过编写异常处理代码来实现。异常考虑越周全,异常处理代码量越大。但是随着代码量的增大,引入Bug的概率也就越大,系统也就越不健壮。从另外一个角度来讲,异常流程处理代码也要考虑健壮性问题,这就形成了无限循环。所以在系统设计和代码编写过程中,奥卡姆剃刀原则要求:一个功能模块如非必要,就不要;一段代码如非必写,就不写。

奥卡姆剃刀原则和最小可用原则有所区别。最小可用原则主要运用于产品MVP阶段,本文所指的奥卡姆剃刀原则主要指系统设计和代码编写两个方面,这是完全不同的两个概念。MVP包含系统设计和代码编写,但同时,系统设计和代码编写也可以发生在成熟系统的迭代阶段。


性能恶化模式

在讲解性能优化模式之前,有必要先探讨一下性能恶化模式,因为:

  1. 很多性能优化模式的目标之一就是避免系统进入性能恶化模式;
  2. 不同性能优化模式可能是避免同一种性能恶化模式;
  3. 同一种性能优化模式可能在不同阶段避免不同的性能恶化模式。
    在此统一阐述性能恶化模式,避免下文重复解释。为了便于读者清晰识别恶化模式和优化模式,恶化模式采用“XXX反模式”的方式进行命名。

长请求拥塞反模式(High Latency Invocating AntiPattern)

这是一种单次请求时延变长而导致系统性能恶化甚至崩溃的恶化模式。对于多线程服务,大量请求时间变长会使线程堆积、内存使用增加,最终可能会通过如下三种方式之一恶化系统性能:

  1. 线程数目变多导致线程之间CPU资源使用冲突,反过来进一步延长了单次请求时间;
  2. 线程数量增多以及线程中缓存变大,内存消耗随之剧增,对于基于Java语言的服务而言,又会更频繁地full GC,反过来单次请求时间会变得更长;
  3. 内存使用增多,会使操作系统内存不足,必须使用Swap,可能导致服务彻底崩溃。
    典型恶化流程图如下图:
    长请求拥塞反模式

长请求拥塞反模式所导致的性能恶化现象非常普遍,所以识别该模式非常重要。典型的场景如下:某复杂业务系统依赖于多个服务,其中某个服务的响应时间变长,随之系统整体响应时间变长,进而出现CPU、内存、Swap报警。系统进入长请求拥塞反模式的典型标识包括:被依赖服务可用性变低、响应时间变长、服务的某段计算逻辑时间变长等。

多次请求杠杆反模式(Levered Multilayer Invocating AntiPattern)

客户端一次用户点击行为往往会触发多次服务端请求,这是一次请求杠杆;每个服务端请求进而触发多个更底层服务的请求,这是第二次请求杠杆。每一层请求可能导致一次请求杠杆,请求层级越多,杠杆效应就越大。在多次请求杠杆反模式下运行的分布式系统,处于深层次的服务需要处理大量请求,容易会成为系统瓶颈。与此同时,大量请求也会给网络带来巨大压力,特别是对于单次请求数据量很大的情况,网络可能会成为系统彻底崩溃的导火索。典型恶化流程图如下图:
多次请求杠杆反模式
多次请求杠杆所导致的性能恶化现象非常常见,例如:对于美团推荐系统,一个用户列表请求会有多个算法参与,每个算法会召回多个列表单元(商家或者团购),每个列表单元有多种属性和特征,而这些属性和特征数据服务又分布在不同服务和机器上面,所以客户端的一次用户展现可能导致了成千上万的最底层服务调用。对于存在多次请求杠杆反模式的分布式系统,性能恶化与流量之间往往遵循指数曲线关系。这意味着,在平常流量下正常运行服务系统,在流量高峰时通过线性增加机器解决不了可用性问题。所以,识别并避免系统进入多次请求杠杆反模式对于提高系统可用性而言非常关键。

反复缓存反模式(Recurrent Caching AntiPattern)

为了降低响应时间,系统往往在本地内存中缓存很多数据。缓存数据越多,命中率就越高,平均响应时间就越快。为了降低平均响应时间,有些开发者会不加限制地缓存各种数据,在正常流量情况下,系统响应时间和吞吐量都有很大改进。但是当流量高峰来临时,系统内存使用开始增多,触发了JVM进行full GC,进而导致大量缓存被释放(因为主流Java内存缓存都采用SoftReference和WeakReference所导致的),而大量请求又使得缓存被迅速填满,这就是反复缓存。反复缓存导致了频繁的full GC,而频繁full GC往往会导致系统性能急剧恶化。典型恶化流程图如下图:
反复缓存反模式
反复缓存所导致性能恶化的原因是无节制地使用缓存。缓存使用的指导原则是:工程师们在使用缓存时必须全局考虑,精细规划,确保数据完全缓存的情况下,系统仍然不会频繁full GC。为了确保这一点,对于存在多种类型缓存以及系统流量变化很大的系统,设计者必须严格控制缓存大小,甚至废除缓存(这是典型为了提高流量高峰时可用性,而降低平均响应时间的一个例子)。反复缓存反模式往往发生在流量高峰时候,通过线性增加机器和提高机器内存可以大大减少系统崩溃的概率。


性能优化模式

水平分割模式(Horizontal partitioning Pattern)

原理和动机

典型的服务端运行流程包含四个环节:接收请求、获取数据、处理数据、返回结果。在一次请求中,获取数据和处理数据往往多次发生。在完全串行运行的系统里,一次请求总响应时间满足如下公式:

一次请求总耗时=解析请求耗时 + ∑(获取数据耗时+处理数据耗时) + 组装返回结果耗时

大部分耗时长的服务主要时间都花在中间两个环节,即获取数据和处理数据环节。对于非计算密集性的系统,主要耗时都用在获取数据上面。获取数据主要有三个来源:本地缓存,远程缓存或者数据库,远程服务。三者之中,进行远程数据库访问或远程服务调用相对耗时较长,特别是对于需要进行多次远程调用的系统,串行调用所带来的累加效应会极大地延长单次请求响应时间,这就增大了系统进入长请求拥塞反模式的概率。如果能够对不同的业务请求并行处理,请求总耗时就会大大降低。例如下图中,Client需要对三个服务进行调用,如果采用顺序调用模式,系统的响应时间为18ms,而采用并行调用只需要7ms。
水平分割模式

水平分割模式首先将整个请求流程切分为必须相互依赖的多个Stage,而每个Stage包含相互独立的多种业务处理(包括计算和数据获取)。完成切分之后,水平分割模式串行处理多个Stage,但是在Stage内部并行处理。如此,一次请求总耗时等于各个Stage耗时总和,每个Stage所耗时间等于该Stage内部最长的业务处理时间。

水平分割模式有两个关键优化点:减少Stage数量和降低每个Stage耗时。为了减少Stage数量,需要对一个请求中不同业务之间的依赖关系进行深入分析并进行解耦,将能够并行处理的业务尽可能地放在同一个Stage中,最终将流程分解成无法独立运行的多个Stage。降低单个Stage耗时一般有两种思路:1. 在Stage内部再尝试水平分割(即递归水平分割),2. 对于一些可以放在任意Stage中进行并行处理的流程,将其放在耗时最长的Stage内部进行并行处理,避免耗时较短的Stage被拉长。

水平分割模式不仅可以降低系统平均响应时间,而且可以降低TP95响应时间(这两者有时候相互矛盾,不可兼得)。通过降低平均响应时间和TP95响应时间,水平分割模式往往能够大幅度提高系统吞吐量以及高峰时期系统可用性,并大大降低系统进入长请求拥塞反模式的概率。

具体案例

我们的挑战来自为用户提供高性能的优质个性化列表服务,每一次列表服务请求会有多个算法参与,而每个算法基本上都采用“召回->特征获取->计算”的模式。 在进行性能优化之前,算法之间采用顺序执行的方式。伴随着算法工程师的持续迭代,算法数量越来越多,随之而来的结果就是客户端响应时间越来越长,系统很容易进入长请求拥塞反模式。曾经有一段时间,一旦流量高峰来临,出现整条服务链路的机器CPU、内存报警。在对系统进行分析之后,我们采取了如下三个优化措施,最终使得系统TP95时间降低了一半:

  1. 算法之间并行计算;
  2. 每个算法内部,多次特征获取进行了并行处理;
  3. 在调度线程对工作线程进行调度的时候,耗时最长的线程最先调度,最后处理。

缺点和优点

对成熟系统进行水平切割,意味着对原系统的重大重构,工程师必须对业务和系统非常熟悉,所以要谨慎使用。水平切割主要有两方面的难点:

  1. 并行计算将原本单一线程的工作分配给多线程处理,提高了系统的复杂度。而多线程所引入的安全问题让系统变得脆弱。与此同时,多线程程序测试很难,因此重构后系统很难与原系统在业务上保持一致。
  2. 对于一开始就基于单线程处理模式编写的系统,有些流程在逻辑上能够并行处理,但是在代码层次上由于相互引用已经难以分解。所以并行重构意味着对共用代码进行重复撰写,增大系统的整体代码量,违背奥卡姆剃刀原则。
    对于上面提到的第二点,举例如下:A和B是逻辑可以并行处理的两个流程,基于单线程设计的代码,假定处理完A后再处理B。在编写处理B逻辑代码时候,如果B需要的资源已经在处理A的过程中产生,工程师往往会直接使用A所产生的数据,A和B之间因此出现了紧耦合。并行化需要对它们之间的公共代码进行拆解,这往往需要引入新的抽象,更改原数据结构的可见域。

在如下两种情况,水平切割所带来的好处不明显:

  1. 一个请求中每个处理流程需要获取和缓存的数据量很大,而不同流程之间存在大量共享的数据,但是请求之间数据共享却很少。在这种情况下,流程处理完之后,数据和缓存都会清空。采用顺序处理模式,数据可以被缓存在线程局部存储(ThreadLocal)中而减少重复获取数据的成本;如果采用水平切割的模式,在一次请求中,不同流程会多次获取并缓存的同一类型数据,对于内存原本就很紧张的系统,可能会导致频繁full GC,进入反复缓存反模式。
  2. 某一个处理流程所需时间远远大于其他所有流程所需时间的总和。这种情况下,水平切割不能实质性地降低请求响应时间。

采用水平切割的模式可以降低系统的平均响应时间和TP95响应时间,以及流量高峰时系统崩溃的概率。虽然进行代码重构比较复杂,但是水平切割模式非常容易理解,只要熟悉系统的业务,识别出可以并行处理的流程,就能够进行水平切割。有时候,即使少量的并行化也可以显著提高整体性能。对于新系统而言,如果存在可预见的性能问题,把水平分割模式作为一个重要的设计理念将会大大地提高系统的可用性、降低系统的重构风险。总的来说,虽然存在一些具体实施的难点,水平分割模式是一个非常有效、容易识别和理解的模式。

垂直分割模式(Vertical partitioning Pattern)

原理和动机

对于移动互联网节奏的公司,新需求往往是一波接一波。基于代码复用原则,工程师们往往会在一个系统实现大量相似却完全不相干的功能。伴随着功能的增强,系统实际上变得越来越脆弱。这种脆弱可能表现在系统响应时间变长、吞吐量降低或者可用性降低。导致系统脆弱原因主要来自两方面的冲突:资源使用冲突和可用性不一致冲突。

资源使用冲突是导致系统脆弱的一个重要原因。不同业务功能并存于同一个运行系统里面意味着资源共享,同时也意味着资源使用冲突。可能产生冲突的资源包括:CPU、内存、网络、I/O等。例如:一种业务功能,无论其调用量多么小,都有一些内存开销。对于存在大量缓存的业务功能,业务功能数量的增加会极大地提高内存消耗,从而增大系统进入反复缓存反模式的概率。对于CPU密集型业务,当产生冲突的时候,响应时间会变慢,从而增大了系统进入长请求拥塞反模式的可能性。

不加区别地将不同可用性要求的业务功能放入一个系统里,会导致系统整体可用性变低。当不同业务功能糅合在同一运行系统里面的时候,在运维和机器层面对不同业务的可用性、可靠性进行调配将会变得很困难。但是,在高峰流量导致系统濒临崩溃的时候,最有效的解决手段往往是运维,而最有效手段的失效也就意味着核心业务的可用性降低。

垂直分割思路就是将系统按照不同的业务功能进行分割,主要有两种分割模式:部署垂直分割和代码垂直分割。部署垂直分割主要是按照可用性要求将系统进行等价分类,不同可用性业务部署在不同机器上,高可用业务单独部署;代码垂直分割就是让不同业务系统不共享代码,彻底解决系统资源使用冲突问题。

具体案例

我们的挑战来自于美团推荐系统,美团客户端的多个页面都有推荐列表。虽然不同的推荐产品需求来源不同,但是为了实现快速的接入,基于共用代码库原则,所有的推荐业务共享同一套推荐代码,同一套部署。在一段时间内,我们发现push推荐和首页“猜你喜欢推荐”的资源消耗巨大。特别是在push推荐的高峰时刻,CPU和内存频繁报警,系统不停地full GC,造成美团用户进入客户端时,首页出现大片空白。

在对系统进行分析之后,得出两个结论:

  1. 首页“猜你喜欢”对用户体验影响更大,应该给予最高可用性保障,而push推荐给予较低可用性保障;
  2. 首页“猜你喜欢”和push推荐都需要很大的本地缓存,有较大的内存使用冲突,并且响应时间都很长,有严重的CPU使用冲突。

因此我们采取了如下措施,一方面,解决了首页“猜你喜欢”的可用性低问题,减少了未来出现可用性问题的概率,最终将其TP95响应时间降低了40%;另一方面也提高了其他推荐产品的服务可用性和高峰吞吐量。

  1. 将首页“猜你喜欢”推荐进行单独部署,而将push推荐和其他对系统资源要求不高的推荐部署在另一个集群上面;
  2. 对于新承接的推荐业务,新建一套代码,避免影响首页推荐这种最高可用性的业务。

缺点和优点

垂直分割主要的缺点主要有两个:

  1. 增加了维护成本。一方面代码库数量增多提高了开发工程师的维护成本,另一方面,部署集群的变多会增加运维工程师的工作量;
  2. 代码不共享所导致的重复编码工作。

解决重复编码工作问题的一个思路就是为不同的系统提供共享库(Shared Library),但是这种耦合反过来可能导致部署机器中引入未部署业务的开销。所以在共享库中要减少静态代码的初始化开销,并将类似缓存初始化等工作交给上层系统。总的来说,通过共享库的方式引入的开销可以得到控制。但是对于业务密集型的系统,由于业务往往是高度定制化的,共用一套代码库的好处是开发工程师可以采用Copy-on-write的模式进行开发,需要修改的时候随时拷贝并修改。共享库中应该存放不容易变化的代码,避免使用者频繁升级,所以并不适合这种场景。因此,对于业务密集型的系统,分代码所导致的重复编码量是需要权衡的一个因素。

垂直分割是一个非常简单而又有效的性能优化模式,特别适用于系统已经出现问题而又需要快速解决的场景。部署层次的分割既安全又有效。需要说明的是部署分割和简单意义上的加机器不是一回事,在大部分情况下,即使不增加机器,仅通过部署分割,系统整体吞吐量和可用性都有可能提升。所以就短期而言,这几乎是一个零成本方案。对于代码层次的分割,开发工程师需要在业务承接效率和系统可用性上面做一些折衷考虑。

恒变分离模式(Runtime 3NF Pattern)

原理和动机

基于性能的设计要求变化的数据和不变的数据分开,这一点和基于面向对象的设计原则相悖。在面向对象的设计中,为了便于对一个对象有整体的把握,紧密相关的数据集合往往被组装进一个类,存储在一个数据库表,即使有部分数据冗余(关于面向对象与性能冲突的讨论网上有很多文章,本文不细讲)。很多系统的主要工作是处理变化的数据,如果变化的数据和不变的数据被紧密组装在一起,系统对变化数据的操作将引入额外的开销。而如果易变数据占总数据比例非常小,这种额外开销将会通过杠杆效应恶化系统性能。分离易变和恒定不变的数据在对象创建、内存管理、网络传输等方面都有助于性能提高。

恒变分离模式的原理非常类似与数据库设计中的第三范式(3NF):第三范式主要解决的是静态存储中重复存储的问题,而恒变分离模式解决的是系统动态运行时候恒定数据重复创建、传输、存储和处理的问题。按照3NF,如果一个数据表的每一记录都依赖于一些非主属性集合,而这些非主属性集合大量重复出现,那么应该考虑对被依赖的非主属性集合定义一个新的实体(构建一个新的数据表),原数据库的记录依赖于新实体的ID。如此一来数据库重复存储数据量将大大降低。类似的,按照恒变分离模式,对于一个实体,如果系统处理的只是这个实体的少量变化属性,应该将不变的属性定义为一个新实体(运行时的另一个类,数据库中的另一个表),原来实体通过ID来引用新实体,那么原有实体在运行系统中的数据传输、创建、网络开销都会大大降低。

案例分析

我们的挑战是提供一个高性能、高一致性要求的团购服务(DealService)。系统存在一些多次请求杠杆反模式问题,客户端一次请求会导致几十次DealService读取请求,每次获取上百个团购详情信息,服务端单机需要支持每秒万次级别的吞吐量。基于需求,系统大体框架设计如下:
恒变分离模式
每个DealService定期从持久层同步所有发生变化的deal信息,所有的deal信息保存在内存里面。在最初的设计里面,数据库只有一个数据表DealModelTable,程序里面也只有一个实体类DealModel。由于销量、价格、用户评价等信息的频发变化,为了达到高一致性要求,服务系统每分钟需要从数据库同步几万条记录。随着美团团购数量的增多和用户活跃度的增加,系统出现了三个问题:

  1. 团购服务网卡频繁报警,由于这是高性能低延时服务,又导致了大量的客户端超时异常;
  2. 频繁的full GC,这是由于每条数据库记录更新都会导致运行系统里面老的DealModel实体被销毁,新的DealModels实体被创建;
  3. 数据库从库滞后主库,使得服务数据一致性降低,原因是数据库系统写数据量巨大。

在对系统进行分析之后,我们采用了如下措施,大大降低了网络传输的数据量,缓解了主从数据库同步压力,使得客户端的超时异常从高峰时候的9%降低到了小于0.01%(低于万分之一):

  1. 将DealModelTable中的销量、价格、用户评价等常变的信息单独构建一张数据表VariableDealModel;
  2. 同时在代码中为销量、价格、用户评价等常变数据创建一个单独的类VariableDealModel;
  3. DealService对两张表进行分别同步;
  4. 如果DealModelTable的记录产生了更新,运行系统销毁老的DealModel实体并创建新的DealModel实体;
  5. 如果只是VariableDealModel的记录产生了更新,只对VariableDealModel的属性进行更改。

缺点和优点

采用恒变分离模式,主要有三个缺点:

  1. 不符合面向对象的设计原则。原本概念上统一的实体被切分成多个实体,会给开发工程师带来一些理解上的困难,因此增加维护成本。进一步而言,这会增加引入额外Bug的概率(实际上面向对象之所以如此受欢迎的一个重要原因就是容易理解)。
  2. 增加了类不变量(Class invariant)的维护难度。很多情况下,Class invariant是通过语言所提供的封装(Encapsulation)特性来维护的。当一个类变成多个类,Class invariant可能会被破坏。如果必须维护Class invariant,而这种Class invariant又发生在不同实体之间,那么往往是把不变的属性从不变实体移到易变的实体中去。
  3. 一张数据库表变成多张,也会增加维护成本。

在如下两种场景下,恒变分离模式所带来的好处有限:

  1. 易变数据导致的操作和传输并不频繁,不是系统主要操作;
  2. 易变数据占整体数据的比例很高,杠杆效应不显著,通过恒变分离模式不能根本性地解决系统性能问题。

总的来说,恒变分离模式非常容易理解,其应用往往需要满足两个条件:易变数据占整体数据比例很低(比例越低,杠杆效应越大)和易变数据所导致的操作又是系统的主要操作。在该场景下,如果系统性能已经出现问题,牺牲一些可维护性就显得物有所值。

大部分系统都是由多种类型的数据构成,大多数数据类型的都包含易变、少变和不变的属性。盲目地进行恒变分离会导致系统的复杂度指数级别的增加,系统变得很难维护,所以系统设计者必须在高性能和高维护性之间找到一个平衡点。作者的建议是:对于复杂的业务系统,尽量按照面向对象的原则进行设计,只有在性能出现问题的时候才开始考虑恒变分离模式;而对于高性能,业务简单的基础数据服务,恒变分离模式应该是设计之初的一个重要原则。

数据局部性模式(Locality Pattern)

原理和动机

数据局部性模式是多次请求杠杆反模式的针对性解决方案。在大数据和强调个性化服务的时代,一个服务消费几十种不同类型数据的现象非常常见,同时每一种类型的数据服务都有可能需要一个大的集群(多台机器)提供服务。这就意味着客户端的一次请求有可能会导致服务端成千上万次调用操作,很容易使系统进入多次请求杠杆反模式。在具体开发过程中,导致数据服务数量暴增的主要原因有两个:1. 缓存滥用以及缺乏规划,2. 数据量太大以至于无法在一台机器上提供全量数据服务。数据局部性模的核心思想是合理组织数据服务,减少服务调用次数。具体而言,可以从服务端和客户端两个方面进行优化。

服务端优化方案的手段是对服务进行重新规划。对于数据量太大以至于无法在一台机器上存储全量数据的场景,建议采用Bigtable或类似的解决方案提供数据服务。典型的Bigtable的实现包括Hbase、Google Cloud Bigtable等。实际上数据局部性是Bigtable的一个重要设计原则,其原理是通过Row key和Column key两个主键来对数据进行索引,并确保同一个Row key索引的所有数据都在一台服务器上面。通过这种数据组织方式,一次网络请求可以获取同一个Row key对应的多个Column key索引的数据。缺乏规划也是造成服务数量剧增的一个重要原因。很多通过统计和挖掘出来的特征数据往往是在漫长的时间里由不同team独立产生的。而对于每种类型数据,在其产生之初,由于不确定其实际效果以及生命周期,基于快速接入原则,服务提供者往往会用手头最容易实施的方案,例如采用Redis Cache(不加选择地使用缓存会导致缓存滥用)。数据服务之间缺乏联动以及缺乏标准接入规划流程就会导致数据服务数量膨胀。数据局部性原则对规划的要求,具体而言是指:1. 数据由尽可能少的服务器来提供,2. 经常被一起使用的数据尽可能放在同一台服务器上。

客户端优化有如下几个手段:

  1. 本地缓存,对于一致性要求不高且缓存命中率较高的数据服务,本地缓存可以减少服务端调用次数;
  2. 批处理,对于单机或者由等价的机器集群提供的数据服务,尽可能采用批处理方式,将多个请求合成在一个请求中;
  3. 客户端Hash,对于需要通过Hash将请求分配到不同数据服务机器的服务,尽量在客户端进行Hash,对于落入同一等价集群的请求采用批处理方式进行调用。

案例分析

我们的挑战来自于美团的推荐、个性化列表和个性化搜索服务。这些个性化系统需要获取各种用户、商家和团购信息。信息类型包括基本属性和统计属性。最初,不同属性数据由不同的服务提供,有些是RPC服务,有些是Redis服务,有些是HBase或者数据库,参见下图:
数据局部性模式1

通常而言,客户端每个用户请求都会触发多个算法。一方面,每个算法都会召回几十甚至几百个团购或者商家ID,团购和商家基础属性被均匀地分配到几十台Redis里面(如下图),产生了大量的Redis请求,极端情况下,一次客户端请求所触发的团购基础数据请求就超过了上千次;另一方面,用户特征属性信息有十几种,每种属性也由单独的服务提供,服务端网络调用次数暴增。在一段时间里,很多系统都进入了多次请求杠杆反模式,Redis服务器的网卡经常被打死,多次进行扩容,提高线程池线程数量,丝毫没有改善。
数据局部性模式2
在对系统进行分析之后,按照数据局部性模式的原则,我们采用了如下手段,彻底解决了系统多次请求杠杆反模式的问题:

  1. 采用大内存服务器存储所有的团购和商家基础信息,每个算法只要一次网络请求就可以获取所有的信息;
  2. 服务端采用多线程方式提供服务,避免了Redis单一线程模式下单个请求慢所带来的连锁效应;
  3. 借鉴类似Bigtable的数据组织方式,将用户的多种特征采用两个维度(用户维度和特征类型)进行索引,确保同一用户的信息只存放在一台机器上面,减少网络调用数量。

缺点和优点

数据局部性模式并不适用于系统初级阶段。在初级阶段,最小可用原则往往是主要设计原则之一,出于两方面的考虑:一方面,在初级阶段,很难预测所要提供服务的数据是否有效而且能够长期使用,以及未来的调用量;另一方面,在初级阶段,工程师可能无法预测最终的调用模式,而不同的调用模式会导致数据局部性方案的设计不同。对于已经大量使用的数据服务,采用数据局部性模式进行重构必然要改变老的调用模式,这一方面会引入新的Bug,另一方面也意味着巨大的工作量。需要特别强调的是,数据处于系统的最底层,对于结构复杂而又重要的数据,重构所带来可靠性、一致性和工作量都是需要权衡的因素。对于请求量比较小的数据服务,即使一次请求会触发严重的请求杠杆效应,但是如果原始触发请求数量在可预见的时间内没有明显变多的迹象,进行数据服务重构可能得不偿失。

数据局部性模式能够解决多次请求杠杆反模式所导致的问题,但它并非大数据的产物,CPU、编译器的设计理念里早就融入了该模式,所以很容易被工程师理解。虽然过度设计在系统初级阶段是一个要尽量避免的事情,但是理解和掌握数据局部性模式对于设计出一个可扩展、可重用的系统有很大帮助。很多成熟的系统因为多次请求杠杆反模式而导致系统频繁崩溃,理解数据局部性模式的原则有助于提高工程师分析解决问题的能力,而在确认了系统存在请求杠杆问题后,数据局部性原则是一件非常锐利的武器。

避免蚊子大炮模式(Avoiding Over-generalized Solution Pattern)

原理和动机

“用大炮打蚊子”本来是大材小用的意思,但是细致想一想,用大炮打蚊子,成功率不高。对于开发工程师而言,一方面为了快速承接业务,按照方案复用原则,总是尽可能地利用现有系统,这使得系统功能越来越强大;另一方面,提高系统的通用性或可重用性也是工程师们在设计系统的一个重要目标。随着这两个过程的相互独立演化,采用通用方案解决特定问题的现象随处可见,形象地说,这就像大炮打蚊子。大炮成本很高,蚊子的数量众多,最终的结局往往是蚊子战胜了大炮。

“避免蚊子大炮模式”是经济原则在运行时系统的运用,它要求采用最节省资源(CPU、内存等)的方法来解决所面临的问题,资源浪费会带来未来潜在的风险。工程师接到一个需求的时候,需要思考的不仅仅是如何复用现有的系统,减少开发时间,还需要考虑现有系统为处理每个新需求访问所需运行时成本,以及新需求的预期访问量。否则,不加辨别地利用现有系统,不仅仅增大了重构风险,还有可能交叉影响,对现有系统所支持的服务造成影响。从另外一个角度讲,工程师在构建一个可重用系统的时候,要明确其所不能解决和不建议解决的问题,而对于不建议解决的问题,在文档中标明潜在的风险。

案例分析

我们的挑战是为移动用户寻找其所在位置附近的商家信息。美团有非常完善的搜索系统,也有资深的搜索工程师,所以一个系统需要查找附近的商家的时候,往往第一方案就是调用搜索服务。但是在美团,太多的服务有基于LBS的查询需求,导致搜索请求量直线上升,这本来不属于搜索的主营业务,在一段时间里面反倒成了搜索的最多请求来源。而搜索引擎在如何从几十万商家里面找最近的几百商家方面的性能非常差,因此一段时间里,搜索服务频繁报警。不仅仅搜索服务可用性受到了影响,所有依赖于LBS的服务的可用性都大大降低。

在对系统分析之后,我们认为更适合解决最短直线距离的算法应该是k-d tree,在快速实现了基于k-d tree的LBS Search解决方案之后,我们用4台服务器轻松解决了30多台搜索服务器无法解决的问题,平均响应时间从高峰时的100ms降低到300ns,性能取得了几百倍的提高。

缺点和优点

避免蚊子大炮模式的问题和数据局部性模式类似,都与最小可用原则相冲突。在系统设计初级阶段,寻求最优方案往往意味着过度设计,整个项目在时间和成本变得不可控,而为每个问题去找最优秀的解决方案是不现实的奢求。最优化原则的要求是全面的,不仅仅要考虑的运行时资源,还需要考虑工程师资源和时间成本等,而这些点往往相互矛盾。在如下情况下,避免蚊子大炮模式所带来的好处有限:在可预见的未来,某个业务请求量非常小,这时候花大量精力去找最优技术方案效果不明显。

在设计阶段,避免蚊子大炮模式是一个需要工程师去权衡的选择,需要在开发成本和系统运行成本之间保持一个平衡点。当很多功能融入到一个通用系统里而出现性能问题的时候,要拆分出来每一个功能点所造成的影响也不是件轻易的事情,所以采用分开部署而共用代码库的原则可以快速定位问题,然后有针对性地解决“蚊子大炮”问题。总的来说,在设计阶段,避免蚊子大炮模式是工程师们进行分析和设计的一个重要准则,工程师可以暂时不解决潜在的问题,但是一定要清楚潜在的危害。构建可重用系统或方案,一定要明确其所不能解决和不建议解决的问题,避免过度使用。

实时离线分离模式(Sandbox Pattern)

原理和动机

本模式的极端要求是:离线服务永远不要调用实时服务。该模式比较简单也容易理解,但是,严格地讲它不是一种系统设计模式,而是一种管理规范。离线服务和在线服务从可用性、可靠性、一致性的要求上完全不同。原则上,工程师在编写离线服务代码的时候,应该遵循的就是离线服务编程规范,按照在线服务编程规范要求,成本就会大大提高,不符合经济原则;从另外一方面讲,按照离线服务的需求去写在线服务代码,可用性、可靠性、一致性等往往得不到满足。

具体而言,实时离线分离模式建议如下几种规范:

  1. 如果离线程序需要访问在线服务,应该给离线程序单独部署一套服务;
  2. 类似于MapReduce的云端多进程离线程序禁止直接访问在线服务;
  3. 分布式系统永远不要直接写传统的DBMS。

案例分析

因为违反实时离线分离模式而导致的事故非常常见。有一次,因为一个离线程序频繁的向Tair集群写数据,每一次写10M数据,使得整个Tair集群宕机。另一次,因为Storm系统直接写MySQL数据库导致数据库连接数耗尽,从而使在线系统无法连接数据库。

缺点和优点

为了实现实时在线分离,可能需要为在线环境和离线环境单独部署,维护多套环境所带来运维成本是工程师需要考虑的问题。另一方面,在线环境的数据在离线环境中可能很难获取,这也是很多离线系统直接访问在线系统的原因。但是,遵从实时离线分离模式是一个非常重要的安全管理准则,任何违背这个准则的行为都意味着系统性安全漏洞,都会增大线上故障概率。

降级模式(Degradation Pattern)

原理和动机

降级模式是系统性能保障的最后一道防线。理论上讲,不存在绝对没有漏洞的系统,或者说,最好的安全措施就是为处于崩溃状态的系统提供预案。从系统性能优化的角度来讲,不管系统设计地多么完善,总会有一些意料之外的情况会导致系统性能恶化,最终可能导致崩溃,所以对于要求高可用性的服务,在系统设计之初,就必须做好降级设计。根据作者的经验,良好的降级方案应该包含如下措施:

  1. 在设计阶段,确定系统的开始恶化数值指标(例如:响应时间,内存使用量);
  2. 当系统开始恶化时,需要第一时间报警;
  3. 在收到报警后,或者人工手动控制系统进入降级状态,或者编写一个智能程序让系统自动降级;
  4. 区分系统所依赖服务的必要性,一般分为:必要服务和可选服务。必要服务在降级状态下需要提供一个快速返回结果的权宜方案(缓存是常见的一种方案),而对于可选服务,在降级时系统果断不调用;
  5. 在系统远离恶化情况时,需要人工恢复,或者智能程序自动升级。

典型的降级策略有三种:流量降级、效果降级和功能性降级。流量降级是指当通过主动拒绝处理部分流量的方式让系统正常服务未降级的流量,这会造成部分用户服务不可用;效果降级表现为服务质量的降级,即在流量高峰时期用相对低质量、低延时的服务来替换高质量、高延时的服务,保障所有用户的服务可用性;功能性降级也表现为服务质量的降级,指的是通过减少功能的方式来提高用户的服务可用性。效果降级和功能性降级比较接近,效果降级强调的是主功能服务质量的下降,功能性降级更多强调的是辅助性功能的缺失。做一个类比如下:计划将100个工程师从北京送到夏威夷度假,但是预算不够。采用流量降级策略,只有50工程师做头等舱去了夏威夷度假,其余工程师继续编写程序(这可不好);效果降级策略下,100个工程师都坐经济舱去夏威夷;采用功能性降级策略,100个工程师都坐头等舱去夏威夷,但是飞机上不提供食品和饮料。

案例分析

我们的系统大量使用了智能降级程序。在系统恶化的时候,智能降级程序自动降级部分流量,当系统恢复的时候,智能降级程序自动升级为正常状态。在采用智能降级程序之前,因为系统降级问题,整体系统不可用的情况偶尔发生。采用智能降级程序之后,基本上没有因为性能问题而导致的系统整体不可用。我们的智能降级程序的主要判定策略是服务响应时间,如果出现大量长时间的响应异常或超时异常,系统就会走降级流程,如果异常数量变少,系统就会自动恢复。

缺点和优点

为了使系统具备降级功能,需要撰写大量的代码,而降级代码往往比正常业务代码更难写,更容易出错,所以并不符合奥卡姆剃刀原则。在确定使用降级模式的前提下,工程师需要权衡这三种降级策略的利弊。大多数面向C端的系统倾向于采用效果降级和功能性降级策略,但是有些功能性模块(比如下单功能)是不能进行效果和功能性降级的,只能采用流量降级策略。对于不能接受降级后果的系统,必须要通过其他方式来提高系统的可用性。

总的来说,降级模式是一种设计安全准则,任何高可用性要求的服务,必须要按照降级模式的准则去设计。对于违背这条设计原则的系统,或早或晚,系统总会因为某些问题导致崩溃而降低可用性。不过,降级模式并非不需要成本,也不符合最小可用原则,所以对于处于MVP阶段的系统,或者对于可用性要求不高的系统,降级模式并非必须采纳的原则。

其他性能优化建议

对于无法采用系统性的模式方式讲解的性能优化手段,作者也给出一些总结性的建议:

  1. 删除无用代码有时候可以解决性能问题,例如:有些代码已经不再被调用但是可能被初始化,甚至占有大量内存;有些代码虽然在调用但是对于业务而言已经无用,这种调用占用CPU资源。
  2. 避免跨机房调用,跨机房调用经常成为系统的性能瓶颈,特别是那些伪batch调用(在使用者看起来是一次性调用,但是内部实现采用的是顺序单个调用模式)对系统性能影响往往非常巨大

总结

Christopher Alexander曾说过:”Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice” 。 尽管Christopher Alexander指的是建筑模式,软件设计模式适用,基于同样的原因,性能优化模式也适用。每个性能优化模式描述的都是工程师们日常工作中经常出现的问题,一个性能优化模式可以解决确定场景下的某一类型的问题。所以要理解一个性能优化模式不仅仅要了解性能模式的所能解决的问题以及解决手段,还需要清楚该问题所发生的场景和需要付出的代价。

最后,本文所描述的性能优化模式只是作者的工作经验总结,都是为了解决由以下三种情况所造成的性能问题:1. 日益增长的用户数量,2. 日渐复杂的业务,3. 急剧膨胀的数据,但是这些远非该领域里面的所有模式。对于文章中提到的其他性能优化建议,以及现在和将来可能碰到的性能问题,作者还会不断抽象,在未来总结出更多的模式。性能问题涉及领域非常广泛,而模式是一个非常好的讲解性能问题以及解决方案的方式,作者有理由相信,无论是在作者所从事的工作领域里面还是在其他的领域里面,新的性能优化模式会不断涌现。希望通过本文的讲述,对碰到同样问题的工程师们有所帮助,同时也抛砖引玉,期待出现更多的基于模式方式讲解性能优化的文章。

参考文献:
[1] Chang F, Dean J, Ghemawat S, et al. Bigtable: A Distributed Storage System for Structured Data
[2] Gamma E, Helm R, Johnson R, et al. Design Patterns-Elements of Reusable Object-Oriented Software. Machinery Industry, 2003
[3] Motlik F. Monolithic Core Versus Full Microservice Architecture
[4] Monolithic Design WikiWikiWeb.
[5] Bovet D P, Cesati M. Understanding the Linux Kernel. 3rd ed. O’Reilly Media, 2005.
[6] Bloch J. Effective Java. 2nd ed. Addison-Wesley, 2008.
[7] Alexander C, Ishikawa S, Silverstein M. A Pattern Language: Towns, Buildings, Construction. Oxford University Press, 1977.

37个最好的学习新东西的网站(译)

忘掉那些在学校或者课堂上学习反而收获甚微的方式吧。这些网站或者APP涵盖科学、艺术和技术。它们会教你一些特别的东东,像用node.js构建APP, 而且大部分是免费的。这不会强制你掌握一个新技能,却能扩展你的知识,甚至促进你的职业。你可以在你喜欢的地方学习或者是你自己的舒服的家里。真的不能再简单了。你还等什么呢?

1、在线课程

edX – 世界上最好的大学的在线课程
Coursera  – 获取世界上最好的课程,在线,免费

Coursmos – 任何时间获取你想要的微课程,任何设备可看
Highbrow – 订阅小的每日课程到收件箱
Skillshare – 激发你的创造力的在线课程和项目
Curious – 在线视频课程来提高你的技能
lynda.com – 学习技术、创造性和商业技能
CreativeLive – 获取世界顶级专家的创造性课程
Udemy – 在线学习现实世界的有用技能

2、学习编程

Codecademy-交互式学习编码,免费
Stuk.io-零基础学习编程
Udacity-获取可被业界承认的技能
Platzi-在线学习设计、市场推广、编码
Learnable-最好的方式学习web开发
Code Scool-亲自动手学习编程
Thinkful-一对一的辅导
Code.org-根据指南现在就开始学习
BaseRails-掌握Ruby on Rails和其他web技术
Treehouse-学习HTML,CSS ,iPhone apps和更多
One Month-一个月内学习编程并构建web应用
Dash-学习制作酷炫的网站

3、和数据打交道

DataCamp-R语言的指南和数据课程
DataQuest-浏览器里学习数据科学
DataMonkey-简单有趣的方式开发你的分析技能

4、学习新的语言

Duolingo-免费学习新语言
Lingvist-200小时内学习一门新的语言
Busuu-免费的语言学习社区
Memrise-用识字卡来学习词汇

5、扩展你的知识

TED-Ed-找到辅助的教育视频
Khan Academy-可交互的非常全面的图书馆
Guides.co-最大的在线指南的搜索
Squareknot-漂亮的指引,一步步的指南
Learnist-学习更专业的内容通过web、纸质、视频
Prismatic-学习社会推荐的一些有趣的内容

6、其它红利

Chesscademy-免费学习国际象棋
Pianu-新的方式学习钢琴,可交互
Yousician-数字时代下你的个人吉他教程

阅读原文

推荐阅读

如何开发一个App(Android)

前言

本篇博客从开发的角度来介绍如何开发一个Android App,需要说明一点是,这里只是提供一个如何开发一个app的思路,并不会介绍很多技术上的细节,从整个大局去把握如何去构思一个app的开发,让你对独立开发一款app的时候有个理解,如果有说的不对的地方,欢迎大家拍砖留言讨论。

开发环境

Android应用层使用的开发语言是Java,自然需要用到Java运行环境,无论你在Window是系统还是Mac系统都需要安装JDK,并且配置它的环境变量,不懂什么叫环境变量的或者不知道怎么配的,请利用好自己的百度技能自行学习。

JDK下载

开发工具

开发工具,Android因为历史原因,前面很长一段时间使用的是Eclipse,我们要开发Android应用的时候需要另外安装Google为它开发的插件ADT,Eclipse这个开源软件很优秀,也很专业,但仅仅只是使用它来开发Android就显得有点鸡肋了,如果没有对Eclipse进行优化的话,会很卡很卡,后面Google实在不愿意寄人篱下,就专门为我们开发者开发了目前很受广大开发者推崇的Android Studio,现在2.0预览版也出来啦,大伙赶紧去下吧。

Android studio

模拟器

Android自带的模拟器一直广受诟病,实在是太卡太慢了,对比人家IOS模拟器是那么的流畅,Android开发者直喊好苦逼啊,不过还好后面出了第三方比原生流畅n倍的模拟器Genymotion,大家可以去下一个个人版的够你平时开发测试用了。最好的办法还是使用真机测试,毕竟真机才是真实的环境。

Genymotion

Android核心基础

前面是准备工作,想开发一款Android app,你最起码要掌握Android的核心基础知识,针对初学Android的童鞋,想一口吃掉一个胖子是不可能的,还是得把基础打牢之后才能完成独立的开发,Android入门不难,学完基础你只是具备开发app的基本条件,想做一个优秀的app还有很长一段路要走,经验是靠不断实践堆出来的,找一套系统的课程去认真学习一下,在线教育的资源很多,比如慕课网,极客学院都有很多不错的学习资源,童鞋自己择优来学习即可。

推广:http://edu.csdn.net/course/detail/545(笔者的课程)

产品开发流程

正常的互联网开发app的流程大致如下:
– 产品规划,定产品方向
– 需求调研,产出需求文档
– 需求评审,修订需求文档
– 产品狗画app线框图提供给射鸡师
– 射鸡师根据线框图设计视觉稿
– 程序猿根据视觉稿搭建UI框架
– 程序猿根据需求文档开发功能
– 测试媛编写测试用例,根据排期进行测试
– 程序猿修复回归测试反馈的bug,提交beta版
– 测试通过,提交给运营喵发布到渠道上线

上面是笔者的经验总结,可能有不太准确的地方,但大致整个流程是这样,我们开发者要清楚自己在整个产品中充当的角色,明白自己的工作职责即可。

快速搭建项目

Android比没有想象那么难,只是刚开始要学习的东西多,一下子消化不了才会比较茫然,笔者写这篇博客就是想帮助大家整理一下开发思路。

快速搭建项目也算是一项技能,而这项技能只能由你自己来完善,刚开始做开发的时候一定会有很多重复性的工作,如果没有意识去提高自己的开发效率的话,可能你的十年工作经验代表的只是一年的经验用了十年而已。

那要怎么做,笔者提供一个自己总结的,仅供参考:
– 定开发规范
– 搭建UI框架
– 选用开发库集成(或自造轮子)
– 第三方服务集成(视情况而定)

定开发规范

一个项目一般不会只有一个人独立开发,就算是独立开发,我们也应该定一下开发规范,一般会有什么样的规范?
– 命名规范
– 代码规范
– 代码管理规范

命名规范

命名规范包括:
– 项目命名
– 包命名
– 类命名、成员变量命名
– 资源文件命名
我们做每一个产品都会有相应的命名,不要用中文来命名,最好用能表达中文意思的英文来表示,例如CSDN客户端,可以用CSDNClient作为命名,我们创建项目的时候可以以它作为Application name。
可以看看以前写的这篇文章:
http://blog.csdn.net/wwj_748/article/details/42347283

代码规范

代码规范视语言而定,开发android使用的是Java,自然要按照Java的编码规范进行编码,比如命名采用驼峰命名法,编码的时候也要符合Android的开发规范,比如UI线程不做任何耗时的操作,像网络请求、数据库操作都需要放到子线程中去做,只有UI的刷新才在UI线程中做,像这些规范,可以根据自身在项目遇到的问题来定,定规范的好处就是减少踩坑的几率,提高开发的质量。

代码管理

对于一个经常更新迭代的产品,不可能由头到尾不变,这个时候我们需要对代码进行分支管理,最好使用git代码仓库对代码进行管理,作为一个合格的开发者连git都不用实在说不过去,还用svn的童鞋赶紧放弃它投入git的怀抱,它的好处是不言而喻的,自己去体会。

搭建UI框架

搭建UI框架需要我们根据产品的导航模式来设计,市场上常用的导航模式有如下图几种:

app导航

我们的app如果不出意外一定是其中的一种导航模式,一般线框图出来我们就应该知道即将要开发的app长什么样子,开发者不必等视觉稿和素材出来才开始动工,我们先大致搭个架子,等视觉稿出来之后我们再做调整。

选用开发库

一般我们app涉及到的库会有:
– UI框架(比如下拉刷新PullToRefresh、侧滑菜单Slidingmenu)
– 网络请求库(比如okhtttp、AndroidAsyncHttp、Volley)
– 数据操作库(比如GreenDao、Ormlite)
– 图片缓存框架(比如Universal-Imageloader)
– 数据解析库(比如Gson)

之所以要选用这些库,肯定是为了避免重复造轮子,在开发效率的角度来说,选用优秀的开源库能大大缩短开发周期和提高开发效率,但从个人提升角度来看的话,我们可能就成了一个只会用API的程序猿了,如果想提升的话,造轮子或者分析这些优秀的源代码是一个不错的途径。

第三方服务集成

我们开发app的时候,肯定会遇到一些需求,比如推送的需求、自动升级、数据统计、社会化分享、用户反馈等等,然而对于一个刚起步的企业或者个人开发者的话,全都要自己去开发的话,那岂不是累死,像推送这种有一定的技术门槛,能做好都能成立一家公司了,所以选用一些第三方服务是一个可选之举。如果说你以后做大了,用第三方怕不好控制,那就自己做呗,有钱任性招兵买马就自己做,谁叫咱有钱呢。


前面这些东西开发一个app够了,开发出来能不能用还得有靠谱的测试,有没有crash,操作流不流畅,体验好不好才会有用户去用。这里不从产品的角度去评判一个app的好与坏,程序员要考虑的是从代码层面、性能层面去让我们的app变得更好。

云测

我们开发完毕之后,需要给测试工程师进行基本的功能需求测试,他们传统的做法就是根据事先写好的测试用例来做回归测试,再把测试出来的bug反馈给工程师,工程师再去修bug,但这样实在是太不靠谱了,有时候我们太在意功能而忽略了一些更重要的东西,那就是体验,给用户最直接的感受就是你这个app够不够惊艳,够不够流畅,用户可能根本就不在乎你这个功能做的有多牛逼。所以我们更应该从非功能性方向去做测试,我们的目的是让用户用的爽,而不是加一些乱七八糟的功能。那怎么测非功能性的一些因素,这里就要提到『云测』这个东西,因为现在设备太多了,如果公司要买一堆设备来做测试,那得多少成本,况且设备更新得太快,你根本就跟不上,所以就有了云测这个东西,它是一个云测试平台服务,提供了一大批主流机型,我们就直接省去购买设备的成本,还能得到完善的测试报告。

再来说一下它的好处:
– 终端云,省去测试设备购买租赁成本
– 高效率 节省测试人员成本及时间
– 包含兼容性测试、性能测试、功能测试
– 操作简单、详细测试报告生成

这么多好处,你在缺少测试工程师的时候,不去尝试那实在说不过去。

打包上线

前面的开发环节、测试环节都没问题之后,你离实现一个app的完整开发就不远了,正常的互联网公司,会把签名打包的apk给到运营,交给他们去写文案,上传到应用渠道,让渠道给我们去首发和推广。如果是个人开发者,这些工作就得我们自己做了。

总结

本篇博客从整个app开发流程上去给大家梳理了一遍,没有讲太多技术上的东西,但理解app流程对自己把握产品很有益处,虽然我们只是一个小小的开发者,但如果你有追求,哪天轮到你去负责一个产品的时候,你就应该对开发的每一个环节都了如指掌,因为出了问题,你也知道怎么针对性的去解决。笔者虽然只是一个小小的开发者,但也乐于思考,很多时候不太愿意被别人牵着鼻子走,因为我们的人生就应该把握在自己手里。

【年度案例】小米抢购限流峰值系统「大秒」架构解密

马利超

小米科技的系统研发与大数据工程师,2013年毕业于大连理工大学,毕业后有幸加入小米抢购系统团队,并参与了小米抢购系统开发、重构与调优。其人热爱技术,对分布式系统架构、高并发峰值系统、大数据领域、反作弊领域、搜索/广告/推荐系统有浓厚的兴趣。

上文介绍了【年度案例】小米抢购限流峰值系统架构历年演进历程 ,本文主要介绍最新版「大秒」系统架构。

整合的抢购限流峰值系统——「大秒」

2014年初,公司决定举办一场“米粉节”活动,全天6轮活动,多个国家、多款爆品同时参与抢购。业务场景将变得更加复杂,当天的并发压力也会有一个量级的提升,原有的抢购系统已经不能适应如此复杂的业务场景了。
为此,小米网技术团队基于对 golang 应对高并发、大规模分布式系统能力的肯定,完全基于 golang,重新设计了抢购系统,也就是我们目前使用的抢购限流峰值系统——“大秒”。

在整个系统设计的之初,我们充分考虑了

  1. 灵活性及可运营性;
  2. 可运维性及可伸缩性;
  3. 限流与抢购放号的精准性;

从大秒第一天诞生到演化至今有很多次重构与优化,但一直沿用了设计之初的结构,接下来我们一起了解下小米网抢购限流峰值系统当前的架构以及填过的一些坑。

大秒系统的架构设计

大秒系统主要由如下几个模块构成

  1. 限流集群 HTTP 服务
  2. 放号策略集群 Middle 服务
  3. 监控数据中心 Dcacenter
  4. 监控管理体系 Master
  5. 准实时防刷模块 antiblack
  6. 基础存储与日志队列服务: Redis 集群、Kafka 集群等

整个大秒体系中大秒前端模块 (HTTP/middle/antiblack) 和监控数据中心使用 golang 开发,大秒监控管理体系使用 Python + golang 开发。

大秒的前端架构设计

大秒前端的架构设计从三个系统展开

  1. 限流集群 HTTP 服务
  2. 策略集群 Middle 服务
  3. 准实时反作弊 antiblack 服务

1、限流集群 HTTP 服务

抢购高峰时,通常会有几百万的用户同时请求,瞬时流量非常大,HTTP 集群顶在最前线,接受用户的请求,将合法的请求发送的处理队列,处理队列设置一定的长度限制,通常情况下,抢购用户数与销售商品的比例在100:1,甚至更高,为了避免系统不被冲垮,保障绝大多数用户的体验,我们认为流量是部分可丢失的,当处理队列满时,丢弃入队请求;

虽然设计上过载流量是部分可丢弃的,但是策略层处理能力是非常 power 的,即便是需要丢弃流量,也是按流量的恶意程度,逐级丢弃的,正常用户购买请求不受影响。

我们使用基于规则的识别、离线画像信息、机器学习逻辑回归等方法,识别恶意用户,在系统高负载的情况下,这部分请求可以优先阻击其发送到策略层,优先处理正常用户的请求,保障用户体验过。

HTTP集群中不同节点之间的所持用的状态数据是一致的,处理逻辑也是一致的,所以整个集群中的任何一个节点挂掉,在前端负载均衡能力下,服务的准确性与一致性不受任何影响。

2、策略集群 Middle 服务
HTTP 模块将满足条件用户的请求按照 uid 哈希的规则,转发到 Middle 集群中相应的节点,Middle 集群根据商品放号策略判断 (uid:sku:time) 组合是否可以分配购买资格,并返回给相应的 HTTP 服务;

使用 Middle 服务本地内存维护用户的购买记录信息,支持各种购买规则,比如:单次活动不限购买数量,单次活动仅限购买一款商品,单次活动每款商品仅限购买一次。

我们将 Middle 的放号逻辑抽象成一个有限状态机,由商品的放号策略配置阈值来触发放号状态转换,整个配置由 Master 节点统一管理与调度。

为了提升整个系统的处理能力,我们将用户状态数据局部化,单用户(uid)的所有相关信息全部路由到一台 Middle 节点上处理。

但是有一点风险是,Middle 集群中服务可能会出现活动过程中挂掉的风险,在抢购场景下,商品基本上是瞬时卖完,为了保障系统的处理能力,我们主要从代码层面做优化,review 代码逻辑,保证服务应对异常的处理能力。

虽然理论上存在风险,但是在实际工程中,经历过几百次活动,还没出现 Middle 节点挂掉的情况。

3、准实时防刷 antiblack 服务

基于日志流的防刷架构,在每台 HTTP 节点上部署日志收集 Agent,使用高吞吐量的 Kafka 做日志转储队列,antiblack 模块实时分析用户请求日志,基于 IP 粒度、Uid 粒度等做防刷。

虽然此处将 antiblack 模块定义为准实时防刷模块,但是作弊信息识别的延迟时长在 1 分钟之内,其中主要的时延发生在日志的转储过程中。

大秒的监控管理体系

1、监控数据中心 dcacenter

监控数据中心数据种类

(1) 业务级数据:过大秒的商品配置数据与实时状态数据,当前活动的配置与状态数据等;
(2) 系统级数据: 大秒前端服务集群通信地址配置,限流队列初始长度配置,系统服务资源占用情况,包括:CPU、MEM、连接数等;

数据采集方式

同时使用push和pull模式采集业务级监控数据和系统级监控数据,业务级数据越实时越好,做到1秒采集处理,3秒可视化;

对于 HTTP 节点和 Middle 节点采用pull的模式拉去系统监控数据和业务监控数据,优点如下

(1) 灵活性高
由数据中心控制监控数据采集的粒度,在数据中心处理能力既定的情况下,可以根据前端集群的伸缩规模,灵活的调整数据采集的粒度,比如米粉节时,大秒前端集群扩容至过百台,管理的过大秒商品的数量在400个左右,业务级监控数据量很大,此时监控数据采集时间间隔很容易降配至 2s。

对于除Http服务和Middle服务之外的服务集群,如:redis,管理平台各个模块等可以使用监控数据采集agent,将采集到的数据周期性的push到redis队列,dcacenter采集协程实时的从redis队列中拉去消息,对于基础服务以及python实现的服务,增加了监控数据采集灵活性。

(2) 增强服务的可靠性与伸缩性

大秒在设计之初采用push的方式,在每台前端机器上部署一个数据采集agent,agent和大秒前端服务同时alive,才代表抢购系统健康运行。这样即增加了系统的不稳定因素,由不利于系统的伸缩,将监控数据采集逻辑内置到前端golang程序中,提供tcp管理端口,在数据中心使用pull方式采集数据,很好的解决了这个问题。减少了服务的数量,增强了整个系统的可靠性与伸缩性。

数据ETL与数据缓存

dcacenter同时负责将采集到的业务级数据及系统级监控数据,实时清洗,提取,转换,结构化,并将结构化的数据存储在自身内存中,定制通信协议(golang实现类redis通信协议),作为一个数据中心,对整个管理体系Master及其他系统提供实时数据支持。

将dcacenter直接作为数据中心,主要是出于数据的实时性考虑,省去中间转储环节,上层可视化系统、自动化活动控制系统、规则引擎系统等可以第一时间获得前端实时的销售状态数据及服务的状态数据。

2、监控管理中心 Master

监控管理中心的主要模块如下。

a.仓储库存同步服务StockKeeper
同步商品的仓储系统中的实时库存到秒杀系统,大秒系统拥有双库存保障,一个是实时仓储库存,一个是虚拟库存也就是资格号,在抢购场景下只有当两个库存都有货时,才能正常销售。

b.商品策略控制器PolicyKeeper
基于相应的策略触发器(时间区间与库存区间),当策略触发时,比如12点整,抢购开始,为相应的商品配置策略,并向大秒前端广播商品配置变更命令,在通信基础模块的保障下,整个过程秒级内完成。

c.活动自动化控制ActKeeper
基于监控数据中心获取大秒前端的实时销售数据,自动化的控制活动中的各个状态,活动开始前逐层打开开关,活动开始时打开最后开关,活动过程中维护活动的售罄状态,活动结束后初始化,整个抢购活动的过程无需人工介入;

d.数据可视化
从监控数据中心提取实时的结构化系统级监控数据和业务级监控数据,将活动过程中的详细数据实时可视化到管理页面上,让运营与销售以及大秒管理员能够及时了解当前活动状态,并人工干预活动;

e.监控规则引擎
监控规则引擎建立在监控数据中心之上,根据结构化监控数据判断当前整个抢购系统的状态,及时报警,以及半自动化控制。

f.其他
大秒管理端管理大秒前端所有的数据、配置以及状态,Master体系提供了详细的管理工具与自动化服务。如果清理大秒前端Middle服务中的用户购买信息等。

3、大秒配置管理数据流

整个抢购系统由 Master 体系中各个服务做统一的控制的,Master 控制商品状态及配置数据的变更,控制当前活动的状态,控制商品放号的策略等。

为了保证时效性,商品、活动、系统等配置状态的变更都需要将变更命令广播前端集群,这期间发生了大量的分布式系统间通信,为了保障命令及时下行,我们提取出了命令转发服务:MdwRouter,用于广播控制命令到大秒前端集群。该服务模块维护了到大秒前端长连接,接收 Master 下发的控制命令,并瞬时广播,保障了整个控制流的处理能力。

举个例子,2015 年米粉节,我们单机房大秒集群的规模在过百台级别,假设为 100 台,管理的独立的商品id的数量在 400 个左右,在这种量级的活动下,商品的放行策略是批量管理的,比如我们根据后端交易系统的压力反馈,调整所有商品的放行速度,这时候需要广播的命令条数在: 100*400=40000 级别,Mdwrouter 很好的保障了系统命令下行的速度,秒级完成命令下行。

小米抢购技术架构

1、小米抢购服务闭环设计

小米网抢购系统服务见上图

  1. bigtap体系中大秒前端服务负责抢购时限流放号,并控制放号策略以及维护用户在本地缓存中的购买记录。
  2. cart服务验证token的有效性,并向counter服务发起销量验证请求;
  3. counter服务是整个抢购系统最终的计数器, 海量的请求在bigtap服务的作用下已经被限制在可以承受的压力范围内,并且复杂的放号策略已经在大秒Middle服务中实现,counter只负责最终的计数即可。counter服务采用redis记录相应商品的放号情况,根据预设的销量,判断当前请求加购物车商品是否有库存余量,并维护商品销量;
  4. bigtap体系中的dcacenter服务实时采集商品销量,Master中活动自动化控制服务依据商品销量判断当前商品是否售罄,售罄则通过设置商品的售罄状态,并通知大秒前端;

2、2015年米粉节介绍

从上述整个服务闭环设计可以看出,大秒的功能完全可以抽象成限流系统,只有在处理抢购活动时,数据的管理与一致性要求才使整个系统变得复杂。

2015年米粉节,我们完全使用大秒的限流功能,不限用户的购买数量,很便捷的将系统部署在两个机房,一个物理机房,一个公有云集群,两者同时服务,大秒系统作为整个商城的最前端,能够根据后端服务的压力状态,瞬时调整整个集群放行流量大小,非常好的保障了整个米粉节的正常举行。
在上述文章中,已经介绍了一些服务设计的出发点,每一次优化的背后,都至少有一次惨痛的经历。

大秒系统架构的几点经验总结

1、Golang GC 优化方法

我们从 golang 1.2 版本开始在线上抢购系统中大规模使用,最初上线的 TC 限流集群在抢购的过程中通过过载重启的方式瘸腿前行。

在当前的大秒系统中,对于限流集群主要是 goroutine 资源、HTTP 协议数据结构、TCP 连接读写缓冲区等频繁动态开销,造成内存 GC 压力大,在现有 GC 能力下,我们对 GC 优化从以下几个方面考虑

  1. 减少垃圾产生:降低数据结构或者缓冲区的开销;
  2. 手动管理内存:使用内存池,手动管理内存;
  3. 脏数据尽快释放,增大空闲内存比。

我们使用了以下 3 种 golang GC 优化方法

1)定制 golang HTTP 包

调整 HTTP 协议 conn 数据结构默认分配读写缓冲区的大小,以及手动维护读写缓存池,减少动态开辟内存的次数,降低 GC 压力。

在 Go 语言原生的 HTTP 包中会为每个请求默认分配 8KB 的缓冲区,读、写缓冲区各 4K。而在我们的服务场景中只有 GET 请求,服务需要的信息都包含在 HTTP header 中,并没有 body,实际上不需要如此大的内存进行存储,所以我们调小了读写缓冲区,将读缓冲区调小到 1K,写缓冲区调小到 32B,golang 的 bufio 在写缓冲区较小时,会直接写出。
从 golang 1.3 开始,HTTP 原生的包中已经使用了sync.Pool 维护读写缓存池,但是 sync.Pool 中的数据会被自动的回收,同样会小量的增加 GC 压力,我们此处自己维护缓存池来减少垃圾回收。

2)加快资源释放
原生的 HTTP 包默认使用 keep-alive 的方式,小米抢购场景下,恶意流量占用了大量的连接,我们通过主动设置 response header 的 connection 为 close 来主动关闭恶意连接,加快 goroutine 资源的释放。

3)升级版本
跟进使用 golang 最新的版本,golang 后续的每个版本都有针对 GC 能力的调整。

得益于开源技术力量,以及大秒系统在 GC 优化上的努力,以及系统层的调优,我们的 HTTP 限流层已经可以余量前行。

从上图可以看出,得益于 GC 的优化,2015 年米粉节,每轮抢购,HTTP 服务的内存不会有特别大的抖动。

2、HTTP 服务器内存调优之操作系统参数调整

我们的服务场景下绝大多数的请求数都是恶意请求,恶意请求通常都是短连接请求,大量的短连接会处于 timewait 状态,几分钟之后才会释放,这样会占用大量的资源,通过调整内核参数,尽快释放或者重用 timewait 状态的连接,减少资源的开销。
具体参数调整如下:

net.ipv4.tcp_tw_recycle = 1 (打开TIME-WAIT sockets快速回收)
net.ipv4.tcp_tw_reuse = 1 (允许TIME-WAIT sockets复用)
net.ipv4.tcp_max_tw_buckets=10000  (降低系统连接数和资源占用,默认为18w)

高并发场景下,操作系统层网络模块参数的调整,会起到事半功倍的效果。

3、没有通信就谈不上分布式系统

整个大秒系统模块之间面临的通信要求是非常苛刻的,Master 节点与 HTTP、Middle 节点要频繁的广播控制命令,dcacenter要实时的收集 HTTP、Middle 节点的监控管理数据,HTTP 要将用户的购买请求路由到 Middle 节点之间,Middle 节点要返回给相应的 HTTP 节点放号信息;

我们基于 TCP 定制了简单、高效的通信协议,对于 HTTP 层和 Middle 层通信,通信模块能够合并用户请求,减少通信开销,保障整个大秒系统的高效通信,增加服务的处理能力。

4、服务闭环设计

从上述抢购的服务闭环架构中可以看出,整个抢购流程处理bigtap系统之外,还有 cart 服务,中心 counter 服务,这三者与 bigtap 系统构成了一个数据流的闭环,但是在大秒最初的设计中,是没有 counter 服务的,Middle层策略集群在放号的同时,又作为计数服务存在,但是整个抢购流程却是以商品加入购物车代表最终的抢购成功,这在设计上有一个漏洞,假如 bigtap 计数了,但是token 并没有请求加购物车成功,这是不合理的。为了保证整个系统的准确性,我们增加了计数器服务,计数操作发生在加购物车下游,bigtap 在从计数中心取出商品实时销量,由此,构成一个服务闭环设计。在提升了系统的准确性,同时也保证了用户体验。

5、技术的选择要可控

我们一开始选择使用 ZooKeeper 存放商品的配置信息,在抢购活动的过程伴随着大量的配置变更操作,ZooKeeper 的 watch 机制不适合用于频繁写的场景,造成消息丢失,大秒前端集群状态与配置不一致。

后来,我们将所有的配置信息存放在 Redis 中,基于通信模块,在发生配置变更时,伴随着一次配置项变更的广播通知,大秒前端根据相应的通知命令,拉取 Redis 中相应的配置信息,变更内存中配置及状态。

大秒的几点设计原则

  1. 分治是解决复杂问题的通则;我们从第一代抢购系统演进到当前的大秒系统,衍生出了很多服务,每个服务的产生都是为了专门解决一个问题,分离整个复杂系统,针对每个服务需要解决的问题,各个击破,重点优化。由此,才保障了秒杀体系整体性能、可靠性的提升;
  2. 服务化设计;系统解耦,增强系统的伸缩性与可靠性;
  3. 无状态设计,增强系统的伸缩性,提升集群整体处理能力;
  4. 状态数据局部化,相对于数据中心化,提升集群整体处理能力。
  5. 中心化监控管理,热备部署,既保证了服务的高可用性,又能够提升开发和管理效率。随着集群规模的增大以及管理数据的增多,分离管理信息到不同的数据管理节点,实现管理能力的扩容。通常情况下,中小型分布式系统,单机管理能力即可满足。
  6. 避免过度设计,过早的优化;小步快跑,频繁迭代。
  7. 没有华丽的技术,把细小的点做好,不回避问题,特别是在高并发系统中,一个细小的问题,都可以引发整个服务雪崩。
( generated by haroopad )

Q&A

1、实时仓库怎么避免超卖?

我们的抢购系统以加入购物车代表购买成功,因为用户要买配件等,库存是由计数器控制的,先限流,在计数,在可控的并发量情况下,不会出现超卖。

2、有了放号系统计算放号规则,为什么还需要一个外围的 counter?

主要是 bigtap 到 cart 的环节 token 有丢失,在 cart 之后再加一个计数器,保障销量,bigtap 再读取计数器的数据控制前端商品销售状态,整个延迟不超 3s。

3、HTTP 集群通过 uuid hash 到 Middle,如果目标 Middle 已经死掉怎么应对?

这个问题在文章中有强调,在我们的场景下,商品迅速卖完,这块没有做高可用,只是从代码层面做 review,完善异常处理机制,并且通常情况下,middle 负载不是特别高,几百次活动下来,还没出现过挂掉情况。

4、防刷系统是离线计算的吗,还是有在线识别的策略?

基于日志,准实时,因为请求量比较大,专门搭了一套 Kafka 服务转储日志,基于 golang 开发 logcollect 与 antiblack 模块,可以达到很高的处理性能。

5、请问如何模拟大量请求做测试?

我们遇到的情况是,由于压测机单机端口限制造成早期不好测试,我们这边压测团队基于开源模块开发了能够模拟虚拟IP的模块,打破了单机端口的限制。

6、即使广播和 Redis 拉取商品配置信息,仍有可能配置信息不一致如何解决?

这个主要是商品的配置和状态信息,不涉及到强一致性要求的场景,我们这样可以在秒级达到最终一致性。

想更多了解小米抢购的架构,可阅读抢购系统架构演进介绍 【年度案例】小米抢购限流峰值系统架构历年演进历程