系统设计之初应该考虑到的问题

一个起码合格的系统,在设计之初就应该考虑到各个方面,不求完备,起码也要深思熟虑。当然,你也可以在接到任务的下一刻,就敲起了键盘,然后在不断地迭代开发中发现问题,解决问题。但是,一些问题,真的是在设计之初就可以预见的。这里只是以自己的经历,提出自己的拙见,并且不断提醒自己,这个问题下一次一定要考虑到。

    1.日志

日志的存在绝对不是某些人嘴中的所谓软件工程的条条框框的面子工程,而是一个系统中最重要的、不可或缺的部分。日志的存在除了记录系统运行时的一些数据,更多也起到一种证据和数据追溯的作用。当系统复杂到一定程度后,往往会出现一些莫名其妙的错误,但是又十分难于复现或在短时间内通过阅读代码找到原因,这个时候,日志就起到了重要的作用。通过日志,记录下每一步关键操作,业务流程是在哪 里中断的,这对于快速排查问题是十分重要的。另外,在一些与外部交互的API中,日志也记录了Request和Response,这为定位原因提供 了依据,不至于在出现问题时相互扯皮。短信没发送成功,谁的错?用户还是程序员,或者接口提供商,或者某团体又在开会了?邮件没收到,又是谁的错?当然,记录日志并不是为了真的去找谁的麻烦,而是为了定位问题,解决问题。

记录日志时,一定要做好归类,分门别类记录不同类型的日志,当然,记录的载体随意,可以是文本文件,也可以说数据库。次重要的日志记文本文件,重要的记数据库。

一般我是这么对要记录在文本的日志分类的:

/**
 * 日志类型
 * @desc:
 * @author [email protected]
 * @date:2013-09-12
 */
public enum LogType {
    PAY("PAY"),//支付模块
    BORROW("BORROW"),//资金模块
    USER("USER"),//用户模块
    API("API"),//API模块
    ANY("ANY");//其它任意类型
     
     
    private String type;
    private LogType(String type){
        this.type=type;
    }
    public String getType() {
        return type;
    }
     
}

对于系统中的一些模块,我采用的是request-response模式来设计。在父类中记录日志,然后子类继承父类,调用父类的方法提交参数并获得响应。这样,所有子模块的参数都会被记录下来,而在子模块中,这个日志记录的过程就像不存在一样。这种方式记录的都是重要的日志,和业务结合比较紧密。可以简化为下面的代码:

public class Yreq {
    public String service;
    public int orderNo;
    public final String KEY="ADX8-KOL4-88CD";
    protected HashMap<String, Object> map=new HashMap<>();
     
    public Yreq(){
        orderNo=(int) System.currentTimeMillis();//生成唯一递增请求序列号,此处仅作模拟
        map=data();
    }
     
     
    public HashMap<String, Object> data(){
        map.put("orderNo", orderNo);
        return map;
    }
     
    public void submit(){
        map.put("key", KEY);
        System.out.println("do sth here...");
        System.out.println("log here");
    }
}
 
--
public class MobileApply extends Yreq{
    private String serviceName="MobileApply";
    private String mobile;
    private int userId;
     
    public MobileApply(){
        super();
    }
    /**
     * 构造request参数
     */
    public void Param(){
        map.put("mobile", mobile);
        map.put("userId", userId);
        map.put("servieName",serviceName);
    }
}
 
MobileApply mobileApply=new MobileApply();
        mobileApply.setUserId(5);
        mobileApply.setMobile("15934881123");
        mobileApply.Param();
        mobileApply.submit();


2.测试
测试已经是一个烂了又烂的话题,单元测试、性能压测都属于测试的范围。我觉得比较重要,也需要特意强调的就是单元测试,尤其是单元测试的意义和存在的价值。有这么一个场景,某客户反映我们的系统还款手续费有问题,而我只是一个新来的程序员,并且我们的客户很多 。这个时候一般的做法是这样的:
(1)在客户网站注册一个账号路人甲;
(2)发布一笔借款,发布借款时才发现需要实名认证;
(3)提交实名认证;
(4)后台审批实名认证;
(5)继续发布借款,发现还需要申请信用额度;
(6)申请信用额度;
(7)后台审批信用额度申请
(8)终于好了,现在可以发布借款了。
(9)后台审批借款;
(10)注册账号路人乙;
(11)前台充值金额;
(12)后台审批充值请求;
(13)路人乙借款给路人甲
(14)后台管理员审批这笔借款;
(15)路人甲还款给路人乙;
(16)路人乙查看收到的还款;
    一个小时后,老板揹着手踱过来,站我背后问道“小X呀,客户提到的BUG改好了没呀?”,我只能一边诺诺地回答“我还在测试借款那一步。。额。。貌似有点小问题无法借款”,看着老板摇着脑袋远去的背影,只能暗骂“催!催!催!脑壳有包啊,啷个有额个快,bug我都还没复现呢”。
    问题就出在当流程复杂后,限制条件越来越多,每步流程都有很多先决条件,并且都是繁琐而耗时的手工操作。由于没有单元测试,出现问题时,我必须一步一步来,即使我在之前已经建好了测试账号,也只能省略有限的几步,还是不得不按部就班,花费大量的时间去复现BUG 。有了单元测试后呢?简单多了,我只要按照模块像搭积木一样的拼装各个环节,最后来个断言,直接告诉我过了还是没过。过了,我就可以
闲下来看看小电影,没通过测试?那也不怕,分解开来,一步一步来。
    又比如,我开发了一个新模块,这个模块需要生成十多个新用户进行多轮测试,怎么办?没有单元测试之前,只能傻乎乎地手工注册,1个 ,2个,3个...,后来发现有点傻啊,预先准备了一堆SQL语句,做点修改,放到数据库里执行一下,获取到了ID主键,然后又去资金表里,以这个主键为userId,再插入一条数据。这个节奏貌似有点不对劲啊,傻不傻呀,有单元测试为啥不用?写个循环不就好了。

3.数据指标与监控

    服务器的运行情况如何?内存占用和CPU消耗如何?优化的效果如何?这些都需要数据来说话。话说有一天项目经理安排新来的程序员甲优化 一台服务器,过了半个小时,甲兴冲冲地地跑去对经理说“我优化好了!”,

经理问“你都改了啥?”
甲:“啪啪啪啪。。。”
经理:“性能提升了多少?CPU占用降低了么?相比之前的QPS增加到多少了?”,
甲:“啊?啥?”,
经理:“那我们现在的用户数是多少?”,
甲:“什么??”
经理:“出去!”。

    好了,不言而喻,没有数据的优化都是耍流氓。数据是评判优化的标准,也是优化的方向。转眼三天过去了,项目经理急匆匆地跑来问甲:"XXX的网站突然打不开了,咋搞的!?",甲愣住了,赶紧回忆最近做了啥改动,想想最近也 没干啥啊,好在甲也是受过专业训练的,一点也不慌张,立马想到第二步,看日志。打开Apache日志一看,有个MySQL的错误,说是什么什 么不能写入数据。立马明白了,磁盘空间满了!都怪自己,平时不注意监控数据。磁盘用了多少也没注意,日志生成了一堆又一堆也不管,三个月前的备份都还在。又回到了第一个问题,日志写入是否可控?是否可能写满磁盘?是否有定期清理机制?

4.重发机制
回到第一个问题,短信发送不出去咋办?难道就放着吗?显然不行,有时候发送不了可能是网络的问题,也有可能是数据的问题,而这些问题都是可以解决的。比如我遇到的问题,一个短信发送前需要登陆。但是由于这个接口被多个客户使用,某个客户B手贱,代码中调用了之前遗留的退出操作。而新的流程中并不需要这个退出,而且退出了就不能发短信了。而我们又改不了B的代码,A很郁闷,B还不知道。怎么 办?很简单,在A的代码中send函数加入login函数,一旦A发送失败,根据返回码判断,如果是登录状态不对,就给他重新登录,重发一次试试。

public function sendSMS($arr) {
        $ret = $this->client->sendSMS($arr['mobile'], $arr['msg']);
        //如果被注销了,重新登录,重发,只试一次
        if($ret=='-110'){
            $this->client->login(); 
            $retNew=$this->client->sendSMS($arr['mobile'], $arr['msg']);
        return $retNew;
        }
        return $ret;
    }

当然,这个重发也可以是一直重试直到成功,但是不要犯下和这里类似的错误。

5.多和少的问题
    系统是否设计过度?还是估计不足?按照500QPS标准设计的网站,在第三个月后,随着业务的扩大,还能满足需求不?性能优化和扩展容易不?是否留有缓存接口?有的话,有没有考虑缓存节点故障的可能性?有没有考虑到雪崩的可能性?还是缓存只是个装饰?问题的起因是这样的,我曾经面对这样的一个网站,他的设计是典型的MV架构,对,是MV,不是MVC。少了controll层。他的做法是直接在模板中使用list和module,并且带入参数,从M层获取数据,绕开了C层。这样一来,系统变得极其简单,一点不懂编程的人也能通过在V层里大量用List和循环来显示数据,根本不需要写C层代码,只需要懂HTML和简单的模板语法。问题就在这,几个月后,系统规模扩大了,服务器已经扛不住了,我需要给这个系统加入缓存,遇到问题了,由于没有C层,我很难对缓存进行细粒度控制,V层显然不合适而且也麻烦,M 层也是类似问题,因为我无法保证和统一其它地方的V调用这个M也是这个缓存机制。结果就是M层变得很凌乱,系统体验变得很呆滞。
    类似的问题还出现在很多地方,系统设计之初太短见,导致后期扩展困哪。好的系统设计,5年后还能轻松应对新需求,烂的设计,半年后就很吃力了。那设计过度呢?额,设计过度的系统,我还是愿意设计过度一点也不短视。

    一个合格的系统,还有很多很多需要去考虑,这里就先说这么多吧。

转自:白菜


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章