日志管理的第一件事,就是日志的收集。日志收集是开发者必备的技巧,不管是哪个开发语言,哪个开发平台,日志收集的插件都是有很多选择的。例如:
.net 平台大家钟爱的log4net,支持多种存储方式(文件、数据库),多种格式,多种日志拆分方式。
java 平台主流的log4j、slf4j、logback,多种选择。
日志收集的组件这里就不一一说明了,使用都是很简单的,这里重点说明一下,日志我们收集应该注意的地方:
等级说明debug调试信息info用来收集关注的信息warn警告信息error错误信息
好多开发工程师记录日志总是喜欢用info级别来记录日志,一般的组件默认级别都是info,所有info默认都是会被记录的,而debug信息发布后,是不会被记录的。这是一种偷懒的做法,但这也是很普遍的做法。正确的方式应该根据日志本身的特性去设置日志的级别,其实规范的日志级别是非常重要的:
运营过大型系统的人都知道,除了数据库存储外,日志、图片、附件是存储的三大债主,他们是会占用非常非常大的空间,所有记录info的日志,要简洁易懂,避免空间浪费。 而对于error级别的错误,记录一定要详实,因为error的所有问题,是后期都要去解决的。
为了能很好的反馈当时error产生场景,以上的这些内容都应该被记录,而且越详细越好。
前文说过,error的日志,不仅是我们需要关注的,还是我需要解决掉的问题,所有error日志非常重要。错误日志的收集,必须是全局统一收集的,AOP是你最好的伙伴,如果你发现你的errorr日志收集是在每个类中,到处是
try
{
......
}
catch()
{
log.error("......")
}
这个一定要避免,不管你用那种语言,错误的处理,都是可以通过全局进行统一的处理,错误日志也要通过全局统一收集。
每个开发人员对日志的收集,都是非常熟悉的,基本都是将日志按照日期的方式进行保存,日常使用日志的时候,也是有一些要求:
因为大家都是通过日期方式保存的,但是因为有的人不重视日志,经常会看到有的系统单个日志文件上百M,有的甚至是几G,而实际大家处理问题关注的都是最近的日志,所以控制单个日志文件的大小,对日志的性能以及后期的运维都是非常便利的。
日志文件小才便于浏览,日志最好能通过网址直接访问到,而不需要一波三折登录服务器,花10分钟下载下来,再来分析。
日志内容有时会包含敏感信息,特别是error日志,直接把系统的具体错误抛出来,所以日志除了查看方便,还需要确保日志文件的安全。如果是日志文件是html或者txt,请一定记得把你的日志文件权限修改下,特定用户才能访问,不要随便开放,所有人都能访问。
日志是非常占用存储的空间,日志太大对存储的性能也有一定的影响,所有日志要定期进行清理。
当然,这个也不是一定的,根据每个系统的情况去制定清理计划就可以了。
如果大家是小型网站,一个系统一台服务器,日志管理就简单了。如果系统是做了高可用,后端用了均衡负载,那么,日志存在当前服务器是不太明智的做法,日志一定要统一存储,因为均衡负载随时都可能会切换服务器,当出现故障,你需要去找日志究竟存在哪个服务器,也是件很浪费时间的事情。日志文件也可以通过:
说到日志的同步,就不得不提Logstash这个日志组件。Logstash是现在应用最广的日志收集组件,基于java平台。其实很多java平台的组件,是不用去了解java开发的,只要简单的配置就能使用。
Logstash支持文件同步,也可以结合rsyslog进行文件同步,当然,也支持通过tcp协议,与第三方对接,好伙伴当然是Elasticsearch。Elasticsearch下文也会做简单的介绍。
Logstash中文手册:点击这里
日志的分析也是一个很大的概念,可能对于运维和安全人员关注的是系统的所有日志,包括访问日志、系统监测的日志等,但是开发人员对于日志更多的是:
事实上,在C的世界里面没有特别好的日志函数库(就像JAVA里面的的log4j,或者C++的log4cxx)。C程序员都喜欢用自己的轮子。printf就是个挺好的轮子,但没办法通过配置改变日志的格式或者输出文件。syslog是个系统级别的轮子,不过速度慢,而且功能比较单调。log4c异常坑爹(有内存泄漏、效率低等等),而且已经停止开发。
所以,如果我们对日志系统模块的要求不是很高的话,我们可以考虑自己写一个。
参考博客:用C语言打印日志(Log)
log.h 文件:
/** log.h **/
#ifndef __LOG_H__
#define __LOG_H__
#include "stdio.h"
#include "string.h"
#include "stdlib.h"
#include "time.h"
#include "stdarg.h"
#include "unistd.h"
#define MAXLEN (2048)
#define MAXFILEPATH (512)
#define MAXFILENAME (50)
typedef enum{
ERROR_1=-1,
ERROR_2=-2,
ERROR_3=-3
}ERROR0;
typedef enum{
NONE=0,
INFO=1,
DEBUG=2,
WARN=3,
ERROR=4,
ALL=255
}LOGLEVEL;
typedef struct log{
char logtime[20];
char filepath[MAXFILEPATH];
FILE *logfile;
}LOG;
typedef struct logseting{
char filepath[MAXFILEPATH];
unsigned int maxfilelen;
unsigned char loglevel;
}LOGSET;
int LogWrite(unsigned char loglevel,char *fromat,...);
#endif /* __LOG_H__ */
log.c 文件:
/** log.c **/
#include "log.h"
#define MAXLEVELNUM (3)
LOGSET logsetting;
LOG loging;
const static char LogLevelText[4][10]={"INFO","DEBUG","WARN","ERROR"};
static char * getdate(char *date);
static unsigned char getcode(char *path){
unsigned char code=255;
if(strcmp("INFO",path)==0)
code=1;
else if(strcmp("WARN",path)==0)
code=3;
else if(strcmp("ERROR",path)==0)
code=4;
else if(strcmp("NONE",path)==0)
code=0;
else if(strcmp("DEBUG",path)==0)
code=2;
return code;
}
static unsigned char ReadConfig(char *path){
char value[512]={0x0};
char data[50]={0x0};
FILE *fpath=fopen(path,"r");
if(fpath==NULL)
return -1;
fscanf(fpath,"path=%s\n",value);
getdate(data);
strcat(data,".log");
strcat(value,"/");
strcat(value,data);
if(strcmp(value,logsetting.filepath)!=0)
memcpy(logsetting.filepath,value,strlen(value));
memset(value,0,sizeof(value));
fscanf(fpath,"level=%s\n",value);
logsetting.loglevel=getcode(value);
fclose(fpath);
return 0;
}
/*
*日志设置信息
* */
static LOGSET *getlogset(){
char path[512]={0x0};
getcwd(path,sizeof(path));
strcat(path,"/log.conf");
if(access(path,F_OK)==0){
if(ReadConfig(path)!=0){
logsetting.loglevel=INFO;
logsetting.maxfilelen=4096;
}
}else{
logsetting.loglevel=INFO;
logsetting.maxfilelen=4096;
}
return &logsetting;
}
/*
*获取日期
* */
static char * getdate(char *date){
time_t timer=time(NULL);
strftime(date,11,"%Y-%m-%d",localtime(&timer));
return date;
}
/*
*获取时间
* */
static void settime(){
time_t timer=time(NULL);
strftime(loging.logtime,20,"%Y-%m-%d %H:%M:%S",localtime(&timer));
}
/*
*不定参打印
* */
static void PrintfLog(char * fromat,va_list args){
int d;
char c,*s;
while(*fromat)
{
switch(*fromat){
case 's':{
s = va_arg(args, char *);
fprintf(loging.logfile,"%s",s);
break;}
case 'd':{
d = va_arg(args, int);
fprintf(loging.logfile,"%d",d);
break;}
case 'c':{
c = (char)va_arg(args, int);
fprintf(loging.logfile,"%c",c);
break;}
default:{
if(*fromat!='%'&&*fromat!='\n')
fprintf(loging.logfile,"%c",*fromat);
break;}
}
fromat++;
}
fprintf(loging.logfile,"%s","\n");
}
static int initlog(unsigned char loglevel){
char strdate[30]={0x0};
LOGSET *logsetting;
//获取日志配置信息
if((logsetting=getlogset())==NULL){
perror("Get Log Set Fail!");
return -1;
}
if((loglevel&(logsetting->loglevel))!=loglevel)
return -1;
memset(&loging,0,sizeof(LOG));
//获取日志时间
settime();
if(strlen(logsetting->filepath)==0){
char *path=getenv("HOME");
memcpy(logsetting->filepath,path,strlen(path));
getdate(strdate);
strcat(strdate,".log");
strcat(logsetting->filepath,"/");
strcat(logsetting->filepath,strdate);
}
memcpy(loging.filepath,logsetting->filepath,MAXFILEPATH);
//打开日志文件
if(loging.logfile==NULL)
loging.logfile=fopen(loging.filepath,"a+");
if(loging.logfile==NULL){
perror("Open Log File Fail!");
return -1;
}
//写入日志级别,日志时间
fprintf(loging.logfile,"[%s] [%s] ",LogLevelText[loglevel-1],loging.logtime);
return 0;
}
/*
*日志写入
* */
int LogWrite(unsigned char loglevel,char *fromat,...)
{
int rtv = -1;
va_list args;
//[为支持多线程需要加锁] pthread_mutex_lock(&mutex_log); //lock.
do{
//初始化日志
if(initlog(loglevel) != 0)
{
rtv = -1;
break;
}
//打印日志信息
va_start(args,fromat);
PrintfLog(fromat,args);
va_end(args);
//文件刷出
fflush(loging.logfile);
//日志关闭
if(loging.logfile!=NULL)
fclose(loging.logfile);
loging.logfile=NULL;
rtv = 0;
}while(0);
//[为支持多线程需要加锁] pthread_mutex_unlock(&mutex_log); //unlock.
return rtv;
}
test.c 文件:
/** test.c **/
#include "stdio.h"
#include "stdlib.h"
#include "log.h"
int main(int argv,char**argc){
printf("%s\n",argc[0]);
LogWrite(INFO,"%s","Hello World!");
LogWrite(DEBUG,"%s","H.e.l.l.o W.o.r.l.d!");
LogWrite(WARN,"%s","H e l l o W o r l d!");
LogWrite(ERROR,"%s","Hallo World!");
return 0;
}
path=./temp
level=ALL
该系统需要在类Unix系统下使用。
下面我演示一下在我的Ubuntu云服务器上的使用方法。
1. 按照上面的要求新建代码文件和目录
root@iZ2zeaqy08amcq9fk440kyZ:/code/log1# tree
.
├── log.c
├── log.conf
├── log.h
├── temp
└── test.c
2. 编译
root@iZ2zeaqy08amcq9fk440kyZ:/code/log1# gcc log.c test.c -o out
3. 运行
root@iZ2zeaqy08amcq9fk440kyZ:/code/log1# ./out
4. 可打开temp目录下的日志文件看看
root@iZ2zeaqy08amcq9fk440kyZ:/code/log1# tree
.
├── log.c
├── log.conf
├── log.h
├── out
├── temp
│ └── 2021-12-13.log
└── test.c
5. temp目录下的2021-12-13.log文件既是我们写入的日志文件
文件中的内容如下:
[INFO] [2021-12-13 21:18:25] Hello World!
[DEBUG] [2021-12-13 21:18:25] H.e.l.l.o W.o.r.l.d!
[WARN] [2021-12-13 21:18:25] H e l l o W o r l d!
[ERROR] [2021-12-13 21:18:25] Hallo World!
[INFO] [2021-12-13 21:18:59] Hello World!
可以看到记录了日志等级,日期时间,打印信息。
可以看到这个日志系统还是很小巧易用的,适合与在有文件系统的类Unix系统中。
下面这个日志系统是打印出来的:C语言日志分级设计
/****************************************************************
***Author: lishuangliang ***
***Email: lishuangliang@outlook.com ***
***Date: 2018-09-24 ***
***Festivsl: Mid-autumn ***
*****************************************************************/
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdarg.h>
#include <stdbool.h>
#include <sys/un.h>
#include <sys/param.h>
#include <time.h>
//通过宏来控制是否打开日志输出
#ifdef DISABLE_DEBUG
#define real_debug_level 0
#else
#define real_debug_level debug_level
#endif
//定义日志输出级别
#define FATALEER (1<<0)
#define ERROR (1<<1)
#define WRAN (1<<2)
#define INFO (1<<3)
#define DEBUG (1<<4)
#define WriteLog(level,mesg) log_mesg_printf(__FILE__, __LINE__, __func__, level, mesg)
#define WriteLog2(level,format, arg...) log_mesg_printf2( __FILE__,__FUNCTION__, __LINE__, level, format, ##arg)
int debug_level = 0;
struct dbg {
int level;
const char *mesg;
};
static struct dbg debug_level_table[] = {
{FATALEER, "Config The Log Level as FATALEER"},
{ERROR, "Config The Log Level as ERROR"},
{WRAN, "Config The Log Level as WRAN"},
{INFO, "Config The Log Level as INFO"},
{DEBUG, "Config The Log Level as DEBUG"}
};
void print_debug_usage(void)
{
struct dbg *p;
fprintf(stderr,
" To calculate the debug level, logically 'or'\n"
" some of the following values together to get a debug level:\n");
for (p = debug_level_table;
p <
debug_level_table +
(sizeof (debug_level_table) / sizeof (struct dbg)); p++) {
fprintf(stderr, "\t%d:\t%s\n", p->level, p->mesg);
}
fprintf(stderr, "\n");
}
void parse_debug(char *foo)
{
int i;
struct dbg *p;
if (!foo)
return;
fprintf(stderr, "Before parse_debug, debug_level is: %d\n",
debug_level);
i = atoi(foo);
if(i == -1)
{
/* error */
fprintf(stderr, "Invalid level specified.\n");
exit(0);
}
for (p = debug_level_table;p < debug_level_table +(sizeof (debug_level_table) / sizeof (struct dbg)); p++)
{
if (i > 0) {
if (i & p->level) {
fprintf(stderr, "Enabling %s debug level.\n",p->mesg);
debug_level |= p->level;
}
}
}
fprintf(stderr, "After parse_debug, debug_level is: %d\n",
debug_level);
}
char *get_commonlog_time(void)
{
char *p;
char sys_time[64];
time_t tnow = time(NULL);
struct tm *ptm = localtime(&tnow);
memset(sys_time, 0 ,sizeof(sys_time));
sprintf(sys_time, "%04d-%02d-%02d %02d:%02d:%02d",ptm->tm_year+1900 ,ptm->tm_mon+1 ,ptm->tm_mday ,ptm->tm_hour ,ptm->tm_min ,ptm->tm_sec);
//return (char *)sys_time;
p = sys_time;
return p;
}
void log_mesg_printf(const char *file, int line, const char *func,int level, const char *mesg)
{
if(real_debug_level & level)
{
int errno_save = errno;
fprintf(stderr, "%s%s:%d (%s) - ", get_commonlog_time(), file, line, func);
errno = errno_save;
perror(mesg);
errno = errno_save;
}
}
void log_mesg_printf2(const char *file,const char *func,const int line, int level, char *fmt,...)
{
if(real_debug_level & level)
{
char msg_buf[20*1024];
va_list ap;
va_start(ap,fmt);
sprintf(msg_buf,"[%s %s:%s:%d] ",get_commonlog_time(),file,func,line);
vsprintf(msg_buf+strlen(msg_buf),fmt,ap);
fprintf(stderr,"%s\n",msg_buf);
va_end(ap);
}
}
int main(int argc, char* argv[])
{
#ifdef DISABLE_DEBUG
print_debug_usage();
parse_debug(argv[1]);//解析日志打印输出级别
#endif
//不使用可变参数解析样例
WriteLog(DEBUG,"I want to DEBUG");
WriteLog(INFO,"I want to INFO");
WriteLog(WRAN,"I want to WARN");
WriteLog(ERROR,"I want to ERROR");
WriteLog(FATALEER,"I want to FATALEER");
//使用可变参数解析样例
WriteLog2(DEBUG,"I want to %s which level is %d","DEBUG",DEBUG);
WriteLog2(INFO,"I want to %s which level is %d","INFO",INFO);
WriteLog2(WRAN,"I want to %s which level is %d","WRAN",WRAN);
WriteLog2(ERROR,"I want to %s which level is %d","ERROR",ERROR);
WriteLog2(FATALEER,"I want to %s which level is %d","FATALEER",FATALEER);
return 0;
}
zlog是一个高可靠性、高性能、线程安全、灵活、概念清晰的纯C日志函数库。
事实上,在C的世界里面没有特别好的日志函数库(就像JAVA里面的的log4j,或者C++的log4cxx)。C程序员都喜欢用自己的轮子。printf就是个挺好的轮子,但没办法通过配置改变日志的格式或者输出文件。syslog是个系统级别的轮子,不过速度慢,而且功能比较单调。
所以我写了zlog。
zlog在效率、功能、安全性上大大超过了log4c,并且是用c写成的,具有比较好的通用性。
zlog有这些特性:
相关链接:
主页:http://hardysimpson.github.com/zlog/
下载:https://github.com/HardySimpson/zlog/releases
其实之前我已经写过一篇博客来讲解zlog的简单使用:【C语言开源库】C语言开源库zlog的使用
zlog的目标是成为一个简而精的日志函数库,不会直接支持网络输出或者写入数据库,不会直接支持日志内容的过滤和解析。
原因很明显,日志库是被应用程序调用的,所有花在日志库上的时间都是应用程序运行时间的一部分,而上面说的这些操作都很费时间,会拖慢应用程序的速度。这些事儿应该在别的进程或者别的机器上做。
如果你需要这些特性,我建议使用rsyslog、zLogFabric、Logstash,这些日志搜集、过滤、存储软件,当然这是单独的进程,不是应用程序的一部分。
目前zlog已经支持7.4,可以自己实现一个输出函数,自由的把日志输出到其他进程或者其他机器。而把日志的分类匹配、日志格式成型的工作交给zlog。
目前我的想法是实现一个zlog-redis客户端,用自定义输出功能,把日志存储到本机或者远程的redis服务器内,然后用其他进程(也使用zlog库)来把日志写到文件里面,不知大家以为这个想法如何?欢迎和我联系探讨。
下载[zlog-latest-stable.tar.gz](file://https://github.com/HardySimpson/zlog/archive/latest-stable.tar.gz)
$ tar -zxvf zlog-latest-stable.tar.gz
$ cd zlog-latest-stable/
$ make
$ sudo make install
or
$ sudo make PREFIX=/usr/local/ install
PREFIX指明了安装的路径,安转完之后为了让你的程序能找到zlog动态库
$ sudo vi /etc/ld.so.conf
/usr/local/lib
$ sudo ldconfig
在你的程序运行之前,保证libzlog.so在系统的动态链接库加载器可以找到的目录下。上面的命令适用于linux,别的系统自己想办法。
$ make 32bit # 32bit version on 64bit machine, libc6-dev-i386 is needed
$ make noopt # without gcc optimization
$ make doc # lyx and hevea is needed
$ make test # test code, which is also good example for zlog
应用程序使用zlog很简单,只要在C文件里面加一行。
#include "zlog.h"
链接zlog需要pthread库,命令是:
$ cc -c -o app.o app.c -I/usr/local/include
# -I[where zlog.h is put]
$ cc -o app app.o -L/usr/local/lib -lzlog -lpthread
# -L[where libzlog.so is put]
这些代码在$(top_builddir)/test/test_hello.c, test_hello.conf
写一个C文件:
#include <stdio.h>
#include "zlog.h"
int main(int argc, char** argv)
{
int rc;
zlog_category_t *c;
rc = zlog_init("test_hello.conf");
if (rc) {
printf("init failed\n");
return -1;
}
c = zlog_get_category("my_cat");
if (!c) {
printf("get cat fail\n");
zlog_fini();
return -2;
}
zlog_info(c, "hello, zlog");
zlog_fini();
return 0;
}
写一个配置文件,放在和test_hello.c同样的目录下:
[formats]
simple = "%m%n"
[rules]
my_cat.DEBUG >stdout; simple
编译、然后运行!
$ cc -c -o test_hello.o test_hello.c -I/usr/local/include
$ cc -o test_hello test_hello.o -L/usr/local/lib -lzlog
$ ./test_hello
hello, zlog
这个例子在$(top_builddir)/test/test_default.c, test_default.conf. 源代码是:
#include <stdio.h>
#include "zlog.h"
int main(int argc, char** argv)
{
int rc;
rc = dzlog_init("test_default.conf", "my_cat");
if (rc) {
printf("init failed\n");
return -1;
}
dzlog_info("hello, zlog");
zlog_fini();
return 0;
}
配置文件是test_default.conf,和test_hello.conf一模一样,最后执行程序的输出也一样。区别在于这里用了dzlog API,内含一个默认的zlog_category_t。详见6.5。
zlog有3个重要的概念:分类(Category)、规则(Rule)和格式(Format)。
分类(Category)用于区分不同的输入。代码中的分类变量的名字是一个字符串,在一个程序里面可以通过获取不同的分类名的category用来后面输出不同分类的日志,用于不同的目的。
格式(Format)是用来描述输出日志的格式,比如是否有带有时间戳,是否包含文件位置信息等,上面的例子里面的格式simple就是简单的用户输入的信息+换行符。
规则(Rule)则是把分类、级别、输出文件、格式组合起来,决定一条代码中的日志是否输出,输出到哪里,以什么格式输出。
所以,当程序执行下面的语句的时候
zlog_category_t *c;
c = zlog_get_category("my_cat");
zlog_info(c, "hello, zlog");
zlog会找到c的名字是"my_cat",对应的配置文件中的规则是
[rules]
my_cat.DEBUG >stdout; simple
然后库会检查,目前这条日志的级别是否符合规则中的级别来决定是否输出。因为INFO>=DEBUG,所以这条日志会被输出。并且根据这条规则,会被输出到stdout(标准输出) ,输出的格式是simple,在配置文件中定义是
[formats]
simple = "%m%n"
最后在屏幕上打印
hello, zlog
这就是整个过程。用户要做就是写自己的信息。日志往哪里输出,以什么格式输出,都是库和配置文件来完成的。
好,那么目前这个模型和syslog有什么关系呢?至今为止,这个模型还是比较像log4j。log4j的模型里面有logger, appender和layout。区别在于,在log4j里面,代码中的logger和配置中的logger是一一对应的,并且一个logger有唯一的级别。一对一关系是log4j, log4cxx, log4cpp, log4cplus, log4net的唯一选择。
但这种模型是不灵活的,他们发明了过滤器(filters)来弥补,但这只能把事情弄得更加混乱。所以让我们把目光转回syslog的模型,这是一个设计的很简易正确的模型。
继续上一节的例子,如果在zlog的配置文件中有这么2行规则:
[rules]
my_cat.DEBUG >stdout; simple
my_cat.INFO >stdout;
然后,一行代码会产生两行输出:
hello, zlog
2012-05-29 10:41:36 INFO [11288:test_hello.c:41] hello, zlog
现在一个代码中的分类对应配置文件中的两条规则。log4j的用户可能会说:"这很好,但是只要在log4j里面放两个appender也能做的一样。"所以继续看下一个例子:
[rules]
my_cat.WARN "/var/log/aa.log"
my_cat.DEBUG "/var/log/bb.log"
代码是:
zlog_info(c, "info, zlog");
zlog_debug(c, "debug, zlog");
最后,在aa.log中只有一条日志
2012-05-29 10:41:36 INFO [11288:test_hello.c:41] info, zlog
但在bb.log里面有两条
2012-05-29 10:41:36 INFO [11288:test_hello.c:41] info, zlog
2012-05-29 10:41:36 DEBUG [11288:test_hello.c:42] debug, zlog
从这个例子能看出来区别。log4j无法轻易的做到这一点。在zlog里面,一个分类可以对应多个规则,每个规则有自己的级别、输出和格式。这就让用户能按照需求过滤、多渠道输出自己的所有日志。
大部分的zlog的行为取决于配置文件:把日志打到哪里去,用什么格式,怎么转档。配置文件是zlog的黑话,我尽量把这个黑话设计的简单明了。这是个配置文件例子:
# comments
[global]
strict init = true
buffer min = 1024
buffer max = 2MB
rotate lock file = /tmp/zlog.lock
default format = "%d.%us %-6V (%c:%F:%L) - %m%n"
file perms = 600
[levels]
TRACE = 10
CRIT = 130, LOG_CRIT
[formats]
simple = "%m%n"
normal = "%d %m%n"
[rules]
default.* >stdout; simple
*.* "%12.2E(HOME)/log/%c.log", 1MB*12; simple
my_.INFO >stderr;
my_cat.!ERROR "/var/log/aa.log"
my_dog.=DEBUG >syslog, LOG_LOCAL0; simple
my_mice.* $user_define;
有关单位:当设置内存大小或者大数字时,可以设置1k 5GB 4M这样的单位:
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 byte
单位是大小写不敏感的,所以1GB 1Gb 1gB是等效的。