好库文摘 http://doc.okbase.net/ MEF(可扩展框架)使用总结 http://doc.okbase.net/Yuuuuu/archive/298941.html Yu2 2018/6/19 12:44:17

一般情况下,我们对系统要求:

1.对扩展开发对修改关闭

2.高层模块不应该依赖于低层模块,应该依赖于抽象

实际上,这是遵循了面向对象的设计原则中的开放封闭原则和依赖倒置原则,其所做的事情就是为了提高系统的可扩展能力和对代码解耦。

为了满足上述的要求,我们引用了微软的MEF框架,让他来帮助我们做这些事情。

一、概念

  MEF (Managed Extensibility Framework) 使开发人员能够在其 .NET 应用程序中提供挂钩,以供用户和第三方扩展。可以将 MEF 看成一个通用应用程序扩展实用工具。

  MEF 使开发人员能够动态创建扩展,无需扩展应用程序,也不需要任何特定于扩展内容的知识。这可以确保在编译时两者之间不存在耦合,使应用程序能够在运行时扩展,不需要重新编译。MEF 还可以在将扩展加载到应用程序之前,查看扩展程序集的元数据,这是一种速度更快的方法。

二、原理

  将被我们特殊标记的方法,属性,字段,类型,导入到一个容器中,我们再通过MEF内置的反射来使用容器中被我们导入的各种方法,属性,字段,类型。

三、示例

1.类型

public interface IDBConn
{
    void Conn();
}

[Export("SqlConn", typeof(IDBConn))]
 public class SqlConn : IDBConn
 {
    public void Conn()
    {
        Console.WriteLine("this is SqlServer connection");
    }
 }

  我们的数据库连接可能会是多种连接,如果我们直接写死为SqlServer的连接,显然不符合开放关闭原则,所以我们提供了一个数据库连接的接口IDBConn,然后用SqlServer的连接SqlConn ,去实现数据库连接接口。

  然后利用MEF中的Export将该类型导出至MEF的容器中。

  调用:

public string Test()
{
    // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 
    var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    //创建容器
    var Container = new CompositionContainer(assemblyCatalog);
    //执行组合
    Container.ComposeParts();
    //获得容器中的Export
    IDBConn _conn = IOCContainer.Container.GetExportedValue<IDBConn>("SqlConn");
    _conn.Conn();
    return "";
}    

  这是我们的一种方式,当我们的导出到容器中的类过于庞大的时候,我们可以换一种方式来获得导出。

  MEF提供Lazy加载,Lazy的好处在于,我们可以先声明,到了需要使用的时候才会实际去加载,代码如下:

public string TestLazy()
        {
            // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 
            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            //创建容器
            var Container = new CompositionContainer(assemblyCatalog);
            //执行组合
            Container.ComposeParts();
            //lazy
            var export =Container.GetExport<IDBConn>("SqlConn");
            IDBConn _conn = export.Value;
            _conn.Conn();
            return "";
        }

  到此,我们类型说的就差不多了,但是似乎有一点小问题,如果我需要根据某些特定的东西来判断加载哪个类型怎么办?

  首先,我们先看下Export的定义:

        //
        // 摘要:
        //     通过在指定协定名称下导出指定类型,初始化 System.ComponentModel.Composition.ExportAttribute 类的新实例。
        //
        // 参数:
        //   contractName:
        //     用于导出使用此特性标记的类型或成员的协定名称,或 null 或空字符串 ("") 以使用默认协定名称。
        //
        //   contractType:
        //     要导出的类型。
        public ExportAttribute(string contractName, Type contractType);   

  使用contractName可以满足这个需求,但是MEF提供了更加优雅的方式----元组。

  代码如下:

public interface IDBConnMetadata
    {
        string Version { get; }
    }

    [Export("SqlConn", typeof(IDBConn))]
    [ExportMetadata("Version", "v1.0.1")]
    public class SqlConn : IDBConn
    {
        public void Conn()
        {
            Console.WriteLine("this is SqlServer connection");
        }
    }

public string TestMetadata()
        {
            // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 
            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            //创建容器
            var Container = new CompositionContainer(assemblyCatalog);
            //执行组合
            Container.ComposeParts();

            var export = Container.GetExports<IDBConn, IDBConnMetadata>();
            //筛选version为v1.0.1的
            IDBConn _conn = export.Where(p => p.Metadata.Version.Equals("v1.0.1")).FirstOrDefault().Value;

            _conn.Conn();
            return "";
        }

  到此类型就完全ok了,接下来看一下方法。

2.方法

  方法需要对匿名委托需要有一些了解,不懂的,可以自行查一下,很简单。

public class MySqlConn : IDBConn
    {
        private MySqlConn()
        {

        }

        [Export("Conn", typeof(Action))]
        public void Conn()
        {
            Console.WriteLine("this is MySqlConn connection");
        }

        [Export("GetStr", typeof(Func<string>))]
        private string GetStr()
        { return "随便试试字符串"; }

        [Export("GetLength", typeof(Func<string, int>))]
        public int GetLength(string str)
        {
            return str.Length;
        }

    }

  等一下,上面代码是不是有问题啊?构造方法是私有的,也没有用单例,这怎么调用啊?

  首先这又不是静态方法,不能用类名.方法名的方式调用,并且不能实例化,这样岂不是很蒙圈。

  那么我们试着用MEF写一下调用,代码如下:

public string TestAction()
        {
            TestActionNoPara();
            TestActionWithPara();
            TestActionWithParaAndReturn();

            return "";
        }


        #region  Import Action

        #region Import 无参无返回方法
        private void TestActionNoPara()
        {
            // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 
            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            //创建容器
            var Container = new CompositionContainer(assemblyCatalog);
            //执行组合
            Container.ComposeParts();
            var action = Container.GetExport<Action>("Conn");
            action.Value.Invoke();
        }
        #endregion

        #region Import 无参有返回方法
        private void TestActionWithPara()
        {
            // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 
            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            //创建容器
            var Container = new CompositionContainer(assemblyCatalog);
            //执行组合
            Container.ComposeParts();
            var action = Container.GetExport<Func<string>>("GetStr");
            string message = action.Value.Invoke();
        }
        #endregion

        #region Import 有参有返回方法
        private void TestActionWithParaAndReturn()
        {
            // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 
            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            //创建容器
            var Container = new CompositionContainer(assemblyCatalog);
            //执行组合
            Container.ComposeParts();
            var action = Container.GetExport<Func<string, int>>("GetLength");

            int length = action.Value.Invoke("GetLength");
        }
        #endregion

        #endregion

  完全可以访问,并没有因为私有构造方法而造成不能访问,并且上面的GetStr方法也是私有的,依旧可以访问。

  这就恶心了,方法的访问修饰符完全被MEF破坏了。。。

  感觉这应该算一个bug吧,不过MEF提供了一个属性,可以让方法或者类不能被使用[PartNotDiscoverable]。

3.属性和字段

  研究了好久也不知道这个属性和字段应该应用到哪里,有什么实际的应用场景,不过,还是写下这个怎么用。

[Export("SqlConn", typeof(IDBConn))]
public
class SqlConn : IDBConn { [Export("Description")] public string Description { get { return "this is SqlServer connection"; } } public void Conn() { Console.WriteLine("this is SqlServer connection"); } }
public string TestAttribute()
        {
            // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 
            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            //创建容器
            var Container = new CompositionContainer(assemblyCatalog);
            //执行组合
            Container.ComposeParts();

            var desc = Container.GetExportedValue<string>("Description");

            return desc;
        }

  //字段
  [Import("SqlConn")]
  private IDBConn dbConn;

4.构造方法注入

  我们也可以用构造方法的方式完成注入,代码如下:

#region 构造注入
    [Export("TestB")]
    public class TestB
    {
        private IDBConn _conn;
        [ImportingConstructor]
        public TestB([Import("SqlConn")]IDBConn conn)
        {
            _conn = conn;
        }

        public bool IsInject()
        {
            if (_conn == null)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
    }
    #endregion

public string TestInject()
        {
            // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 
            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            //创建容器
            var Container = new CompositionContainer(assemblyCatalog);
            //执行组合
            Container.ComposeParts();
            var x = Container.GetExportedValue<TestB>("TestB");
            bool result = x.IsInject();

            return result.ToString();
        }

四、改进

  上面就是MEF的一些基本使用方法,但是写了这么久发现了似乎有一点问题啊。

  每次调用都去创建目录,创建容器,执行组合,似乎太繁琐了,而且重复这些操作,明显也会浪费资源,所以我们可以做一下改进,将这些重复步骤提炼出来一个单独的方法,供其他方法调用。

  

public class IOCContainer
    {
        public static CompositionContainer Container { get; private set; }

        private static IOCContainer instance = new IOCContainer();

        private IOCContainer()
        {
            if (Container == null)
            {
                //AssemblyCatalog:表示从程序集中搜索部件的目录
                // 创建一个程序集目录,用于从一个程序集获取所有的组件定义
                var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

                //指定加载某个程序集
                //var directoryCatalog = new DirectoryCatalog("要加载dll的路径");
                //AggregateCatalog:聚合目录,可以添加上面所说的所有目录,从而进行多方面的部件搜索
                //var aggregateCatalog = new AggregateCatalog(assemblyCatalog, directoryCatalog);

                //创建容器
                Container = new CompositionContainer(assemblyCatalog);
                //container = new CompositionContainer(aggregateCatalog);
                //container.GetExportedValue

                //执行组合
                Container.ComposeParts();
            }
        }

        public static IOCContainer RegisterContainer()
        {
            return instance;
        }

    }

  写好了容器,我们可以在Global.asax中调用RegisterContainer方法,其他使用容器的地方,改为IOCContainer.Container.GetExport就ok了,既简化了代码,也节约了资源。

  到这里MEF的总结,就全部完成了,欢迎交流。

]]>
sql server 索引阐述系列二 索引存储结构 http://doc.okbase.net/MrHSR/archive/298940.html 花阴偷移 2018/6/19 12:44:10

一.概述、

  "流光容易把人抛,红了樱桃,绿了芭蕉“ 转眼又年中了,感叹生命的有限,知识的无限。在后续讨论索引之前,先来了解下索引和表数据的内部结构,这一节将介绍页的存储,页分配单元类型,区的存储, 最后简要介绍下系统页存储类型,页中的数据结构。

1.1  页存储

  页是 sql server存储数据的基本单位,大小为8kb, 它存储的类型包括表,索引数据,分配位图,可用空间信息等,页也是可以读写的最小I/0单位。也就是如只需访问一行数据,也会把整个页加载到内存中。一页大小是8192个字节,由三块组成分为页头,数据行 , 行偏移也叫页尾行指针。

  页头:有96个字节,包含的信息如PageID(文件号及页面编号),NextPage下一个页面的文件号及页面编号等。

  数据行:单个数据行最大为8060字节。

  末尾行偏移: 36字节的数组,数组中的每一个值指向页中数据。当插入一行数据时,偏移量从右下角开始存储,插入第二行时偏移量为2存放于1的左边,如下图所示:

  一个页存储有三种分配单元类型: in_row_data (行内数据), row_overflow_data( 行溢出数据), lob_data LOB(大型数据)。

分配单元类型 描述
in_row_data 存放固定长度的列,以及可变长度的列。一行字节<= 8060字节的限制
row_overflow_data 存放<=8000字节的varchar,nvarchar, varbinary类型数据。一行字节>= 8060
lob_data 存放大型对象数据类型值 >=8000节字的varchar(max), nvarchar(max), varbinary(max), 以及xml或clr udt

1.2 区

   区是由8个物理上连续的页组成的单元,用来有效管理页,如是区内的8个页属于同一个表,则这种区叫统一区,如果区内8个页分别属于至少两个不同的表,则这种区叫混合区。当表或索引需要更多空间时,sqlserver会为对象分配一个新区,但对象不足64kb,通常只分配一个页,当drop或truncate表时将释放整个区。 

1.3 存储的关系

   每个数据表对应有一个objectID标识符,  表中有多个索引为 sys.indexes,   表和索引存储在多个分区中 sys.partitions,但至少有一个分区。每个分区下面有8个物理连续页, 页中使用最频繁的文件类型是Data数据。

  下图展示表和索引存储结构以及对应的元数据查询。 分配单元可通过SELECT * FROM sys.system_internals_allocation_units视图查看

二. 索引与表存储关系演示

  2.1 首先建一个RowText表

CREATE TABLE [dbo].[RowText](
    [a] [varchar](3000) NULL
) ON [PRIMARY]

  2.2 根据表名,查到表的标识符objectID:  5575058。 这里显示了表的相关信息如创建时间,修改时间。

select * from sys.objects where name='RowText'

  

      2.3  根据标识符objectID,找到表上的多个分区,这里只有一个区。找到标识hobt_id(是该区里堆或B树结构的标识): 72057594038976512

select * from sys.partitions where object_id=5575058

     

  2.4  再根据hobt_id: 72057594038976512,找到页分配的单元, 这里现只显示了in_row_data  行内数据,信息包括了行内数据使用的总页数,已使用页数,数据页数

select * from sys.allocation_units where container_id=72057594038976512

   

  三. 页分配单元演示

  3.1 上面讲到页分配单元有三种类型,通过下面的脚本可以来查看分析。

SELECT CONVERT(VARCHAR(10),OBJECT_NAME(i.object_id)) AS table_name,
     i.Name AS index_name,i.index_id,i.type_desc AS index_type,
     partition_id,partition_number,p.rows,allocation_unit_id,
     a.type_desc AS page_type_desc,
     a.total_pages,a.used_pages,a.data_pages
    FROM sys.indexes i JOIN sys.partitions p 
ON i.object_id=p.object_id and i.index_id=p.index_id
JOIN sys.allocation_units a ON p.partition_id=a.container_id
WHERE i.object_id=object_id('RowText')

  3.2 查看页存储的行内数据

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[RowText]') AND type in (N'U'))
DROP TABLE [dbo].[RowText]

go
CREATE TABLE [dbo].[RowText](
    [a] [varchar](3000) NULL
) ON [PRIMARY]
insert into RowText
select REPLICATE('a',3000)
insert into RowText
select REPLICATE('a',3000)
insert into RowText
select REPLICATE('a',3000)

  当前一行数据3000字节,小于一行8000字节,所以存放为IN_ROW_DATA类型,新增三行共9000字节,大于了一页8060字节,所以产生了data_pages:2页。通过3.1脚本查看

 

  3.2 查看页存储的行溢出数据

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[RowText]') AND type in (N'U'))
DROP TABLE [dbo].[RowText]
go
create TABLE [dbo].[RowText](
    [a] [varchar](3000) NULL,
    [b] [varchar](6000) NULL
) ON [PRIMARY]
insert into RowText
select REPLICATE('a',3000),REPLICATE('b',6000)

  当前一行数据为9000字节,大于一行8000字节,所以分配了ROW_OVERFLOW_DATA类型,通过3.1脚本查看

  3.3  查看页存储的LOB大型数据

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[RowText]') AND type in (N'U'))
DROP TABLE [dbo].[RowText]
go
CREATE TABLE [dbo].[RowText](
    [a] [varchar](3000) NULL,
    [b] [varchar](6000) NULL,
    [c] [text] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
insert into RowText
select REPLICATE('a',3000),REPLICATE('b',6000),REPLICATE('c',12000)

  当前表包含了text类型,所以分配了LOB_DATA大型数据,通过3.1脚本查看

  

       最后简单总结下:在设计表字段时,一定要与业务相符合,尽量避免建大型字段,产生行溢出和LOB大型数据,从而造成页拆分,查询将需要跨页。原理是当一行超过8060字节时,这类型的值将被移动到一个称为行溢出分配单元页中,在原始页上保留一个24字节指针,指向行外数据,这样就完成了行跨页。 存放大型对象数据类型值时,在原始页上保留一个16字节指针,指向该大型对象值。

 四  页单元文件类型

  我们知道页文件主要是用来存储表和索引的数据,其实还包括其它的存储, 在sqlserver里页文件存储的类型也分9种,如下图所示

页类型

内容

Global Allocation Map (GAM)

它用来标识相应的区是否已经被分区,能标识64000个区,也就是4G空间,如超过4G则分配另一个GAM页 bit=0 表示当前区已被数据使用

.Shared Global Allocation Map(SGAM)

表示该区是否是混合区 bit=1 是混合区且至少有一个数据页被分配

Page Free Space

存储该文件里所有页分配情况,和可用空间信息

Index Allocation Map

表或索引使用该区的信息

Bulk Changed Map

最后一条 backup log 语句之后大容量操作所修改区的信息

Differential Changed Map

最后一条 backup database 语句之更改区的信息

Data

用来存储数据

Row Overflow Page

行溢出的数据

LOB

存放大型对象数据:text,nvarchar(max),varchar(max)…

 五.数据页的结构

  在sql server里有这么多页文件类型,哪个页里是怎么存储的,存储了什么内容呢?下面来查看一个数据页存储的信息

  下面通过partition_id的标识符:72057594038976512 找到first_page 字段 第一页

select * from sys.system_internals_allocation_units where container_id=72057594038976512

  

   由于存储关系,页码是以字节16进制显示0x720000000100,这里需要倒序后就是0x010000000072,

  0x表示:文件组号

  0072从16进制转成10进制为第114页(后四字节代表页的编号)

  通过Dbcc page可以查看页的具体内容,使用之前dbcc page, 先打开dbcc traceon (3604) 告诉sql server将结果返回给客户端 ,dbcc Page 四个参数 如下:

dbid | dbname 该页的数据库ID或数据
Filenum 页面文件号
Pagenum 页面号
Printopt =1 对每条记录打印缓冲报头,页面报头,输出行偏移表

  具体使用如下:

  dbcc traceon (3604)

  dbcc page('test',1,114,1)

  数据页结构输出分为四个主要阶段:buffer,  page header, data,  offset table

  Buffer: 用来标识该数据页在内存中的位置。

  page header: 页头信息

  data:存储的数据

  offset table: 偏移量

  感兴趣的朋友,可以再深入研究。这里粗略讲下。

PAGE: (1:114)


BUFFER:


BUF @0x0000000081FE4640

bpage = 0x0000000081B66000           bhash = 0x0000000000000000           bpageno = (1:114)
bdbid = 5                            breferences = 0                      bcputicks = 222
bsampleCount = 1                     bUse1 = 5256                         bstat = 0xc00009
blog = 0x32159                       bnext = 0x0000000000000000           

PAGE HEADER:


Page @0x0000000081B66000

m_pageId = (1:114)                   m_headerVersion = 1                  m_type = 1
m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x8200
m_objId (AllocUnitId.idObj) = 35     m_indexId (AllocUnitId.idInd) = 256  
Metadata: AllocUnitId = 72057594040221696                                 
Metadata: PartitionId = 72057594038976512                                 Metadata: IndexId = 0
Metadata: ObjectId = 5575058         m_prevPage = (0:0)                   m_nextPage = (0:0)
pminlen = 4                          m_slotCnt = 2                        m_freeCnt = 2070
m_freeData = 6118                    m_reservedCnt = 0                    m_lsn = (27:202:2)
m_xactReserved = 0                   m_xdesId = (0:0)                     m_ghostRecCnt = 0
m_tornBits = 2253409                 

Allocation Status

GAM (1:2) = ALLOCATED                SGAM (1:3) = ALLOCATED               
PFS (1:1) = 0x62 MIXED_EXT ALLOCATED  80_PCT_FULL                         DIFF (1:6) = CHANGED
ML (1:7) = NOT MIN_LOGGED            

DATA:


Slot 0, Offset 0x60, Length 3011, DumpStyle BYTE

Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 3011                   
Memory Dump @0x000000001116A060

0000000000000000:   30000400 01000001 00c30b61 61616161 ?0..........aaaaa 
0000000000000010:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000020:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000030:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000040:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000050:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000060:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000070:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000080:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000090:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000100:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000110:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000120:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000130:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000140:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000150:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000160:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000170:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000180:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000190:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000200:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000210:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000220:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000230:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000240:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000250:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000260:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000270:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000280:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000290:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000300:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000310:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000320:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000330:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000340:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000350:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000360:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000370:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000380:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000390:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000400:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000410:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000420:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000430:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000440:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000450:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000460:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000470:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000480:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000490:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000500:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000510:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000520:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000530:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000540:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000550:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000560:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000570:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000580:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000590:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000600:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000610:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000620:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000630:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000640:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000650:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000660:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000670:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000680:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000690:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000700:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000710:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000720:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000730:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000740:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000750:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000760:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000770:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000780:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000790:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000800:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000810:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000820:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000830:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000840:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000850:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000860:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000870:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000880:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000890:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000900:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000910:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000920:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000930:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000940:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000950:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000960:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000970:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000980:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000990:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A00:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A10:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A20:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A30:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A40:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A50:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A60:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A70:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A80:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A90:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AA0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AB0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AC0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AD0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AE0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AF0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B00:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B10:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B20:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B30:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B40:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B50:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B60:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B70:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B80:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B90:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000BA0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000BB0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000BC0:   616161???????????????????????????????aaa              

Slot 1, Offset 0xc23, Length 3011, DumpStyle BYTE

Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 3011                   
Memory Dump @0x000000001116AC23

0000000000000000:   30000400 01000001 00c30b61 61616161 ?0..........aaaaa 
0000000000000010:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000020:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000030:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000040:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000050:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000060:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000070:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000080:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000090:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000000F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000100:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000110:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000120:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000130:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000140:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000150:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000160:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000170:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000180:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000190:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000001F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000200:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000210:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000220:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000230:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000240:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000250:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000260:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000270:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000280:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000290:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000002F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000300:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000310:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000320:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000330:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000340:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000350:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000360:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000370:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000380:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000390:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000003F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000400:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000410:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000420:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000430:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000440:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000450:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000460:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000470:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000480:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000490:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000004F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000500:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000510:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000520:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000530:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000540:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000550:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000560:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000570:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000580:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000590:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000005F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000600:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000610:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000620:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000630:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000640:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000650:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000660:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000670:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000680:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000690:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000006F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000700:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000710:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000720:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000730:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000740:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000750:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000760:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000770:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000780:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000790:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000007F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000800:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000810:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000820:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000830:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000840:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000850:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000860:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000870:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000880:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000890:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000008F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000900:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000910:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000920:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000930:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000940:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000950:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000960:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000970:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000980:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000990:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009A0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009B0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009C0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009D0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009E0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
00000000000009F0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A00:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A10:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A20:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A30:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A40:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A50:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A60:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A70:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A80:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000A90:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AA0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AB0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AC0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AD0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AE0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000AF0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B00:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B10:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B20:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B30:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B40:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B50:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B60:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B70:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B80:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000B90:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000BA0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000BB0:   61616161 61616161 61616161 61616161 ?aaaaaaaaaaaaaaaa 
0000000000000BC0:   616161???????????????????????????????aaa              

OFFSET TABLE:

Row - Offset                         
1 (0x1) - 3107 (0xc23)               
0 (0x0) - 96 (0x60)                  


DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。

  

]]>
python3+requests:接口自动化测试(二) http://doc.okbase.net/shapeL/archive/298939.html Shapelei 2018/6/19 11:33:55

转载请注明出处:https://www.cnblogs.com/shapeL/p/9188495.html

 

前言:上篇文章python3+requests+unittest:接口自动化测试(一):https://www.cnblogs.com/shapeL/p/9179484.html ,已经介绍了基于unittest框架的实现接口自动化,但是也存在一些问题,比如最明显的测试数据和业务没有区分开,接口用例不便于管理等,所以又对此修改完善。接下来主要是介绍该套接口自动化框架的设计到实现,参考代码的git地址:https://github.com/zhangying123456/python3_interface

 

1.代码框架展示

 

(1)case:存放测试用例数据的,比如请求类型get/post、请求url、请求header、请求数据等;

(2)data:获取excel文件中相应数据的方法封装,获取excel中对应表格内的数据,excel的行列数据等:get_data.py;判断用例直接是否存在依赖关系并获取依赖数据:dependent_data.py;初始化excel文件:data_config.py;

(3)dataconfig:存放请求中涉及到的header、data、cookies等数据;

(4)log:存放测试完成之后生成的日志文件,可以查看日志定位问题;

(5)main:脚本执行的主函数run_test.py

(6)util:通用方法的封装,各种不同断言方式common_assert.py;对excel文件的读写操作operation_excel.py;从请求返回数据中拿取数据作为下一个接口的请求header数据operation_header.py;从json文件中拿取想要的数据operation_json.py;将接口自动化过程中的相关日志输出到log.txt中print_log.py;根据请求类型的不同执行对应的get/post方法runmethod.py;将测试结果以邮件形式发送给相关人员send_mail.py。

 

2.代码实现说明

(1)首先看下用例数据

说明:该用例只是用来覆盖一些接口场景而测试使用的,有兴趣的可以参考源码用自己项目的真实数据来实现

 

 先判断是否执行:如果yes,执行该条用例;如果no,直接跳过该条用例。

执行用例:获取用例的url、请求类型、请求头header、请求数据,request.get/post执行该条接口用例。

在执行用例过程中,会存在特殊情况:(1)比如test_04依赖于test_03,test_04中的请求字段supplier的参数数据来源于test_03的response中value[0].biz字段的数据,所以在执行接口过程中需要判断是否存在依赖关系;(2)比如test_06请求数据需要test_05的response中的cookies数据,所以这种类型接口也要特殊处理。

执行完成后:写入实际结果,与预期结果做对比,进行断言。

(2)看了用例excel后,对基本的流程有个大概了解,现在的问题就是如何拿取对应的数据执行接口得到运行结果

    if is_run:
        url = self.data.get_request_url(i)
        method = self.data.get_request_method(i)
        #获取请求参数
        data = self.data.get_data_value(i)
        # 获取excel文件中header关键字
        header_key = self.data.get_request_header(i)
        # 获取json文件中header_key对应的头文件数据
        header = self.data.get_header_value(i)
        expect = self.data.get_expect_data(i)
        depend_case = self.data.is_depend(i)

举例说明1:请求url数据是存放在excel中,我们通过操作excel文件到特定单元格拿到url数据

    #获取url
    def get_request_url(self,row):
        col = int(data_config.get_url())
        url = self.oper_excel.get_cell_value(row,col)
        return url

举例说明2:请求头header或者请求数据中有的数据为空,所以我们在拿取数据过程中要做判断

    #获取请求数据
    def get_request_data(self,row):
        col = int(data_config.get_data())
        data = self.oper_excel.get_cell_value(row,col)
        if data == '':
            return None
        return data

首先拿取excel中表格中的关键字,再通过关键字去对应json文件拿取具体的请求数据。比如先拿取excel中请求数据中的hotwords,再根据此关键字去json文件读取hotwords的键值数据

    "hotwords": {
        "bizName": "globalSearchClient",
        "sign": "8c8bc3ee9d6c4b7b8a390ae298cb6db5",
        "timeMills": "1524906299999"
    }
    #通过获取请求关键字拿到data数据
    def get_data_value(self,row):
        oper_json = OperationJson('../dataconfig/request_data.json')
        request_data = oper_json.get_data(self.get_request_data(row))
        return request_data
    #根据关键字获取数据
    '''
    dict['key']只能获取存在的值,如果不存在则触发KeyError
    dict.get(key, default=None),返回指定键的值,如果值不在字典中返回默认值None
    excel文件中请求数据有可能为空,所以用get方法获取
    '''
    def get_data(self,key):
        # return self.data[key]
        return self.data.get(key)

 (3)一般的接口都是单接口,即是单独请求,没有上下依赖关系的,针对这种只要模拟请求拿到数据进行断言就可以了。但是实际项目中会存在特殊场景,比如test_03和test04

说明:test_04中,请求数据qqmusic_more中的supplier字段依赖于test_03中的返回数据value[0].biz的值

"qqmusic_more": {
        "bizName": "globalSearchClient",
        "appLan": "zh_CN",
        "musicLimit": "20",
        "imei": "864044030085594",
        "keyword": "fly",
        "timeMills": "1527134461256",
        "page": "0",
        "sign": "17daa7e3e84bd4dfbe9a1bd9a1bd7e62",
        "mac": "90f05205d7b7",
        "sessionId": "43e605b914874cd99b47ac997e19c1a1",
        "network": "1",
        "supplier": "",
        "language": "zh_CN",
    }

先执行test_03,获取依赖的返回数据value[0].biz的值

    #执行依赖测试,获取test_03返回结果
    def run_dependent(self):
        row_num = self.oper_excel.get_row_num(self.case_id)
        request_data = self.data.get_data_value(row_num)
        header = self.data.get_request_header(row_num)
        method = self.data.get_request_method(row_num)
        url = self.data.get_request_url(row_num)
        res = self.method.run_main(method,url,request_data,header,params=request_data)
        return res

    #获取依赖字段的响应数据:通过执行依赖测试case来获取响应数据,响应中某个字段数据作为依赖key的value
    def get_value_for_key(self,row):
        #获取依赖的返回数据key
        depend_data = self.data.get_depend_key(row)
        print(depend_data)  #depend_data打印数据:value[0].biz
        #执行依赖case返回结果
        response_data = self.run_dependent()
        # print(depend_data)
        # print(response_data)

        return [match.value for match in parse(depend_data).find(response_data)][0]

再将value[0].biz值放入test_04请求数据qqmusic_more中的supplier字段中

        if depend_case != None:
            self.depend_data = DependentData(depend_case)
            #获取依赖字段的响应数据
            depend_response_data = self.depend_data.get_value_for_key(i)
            #获取请求依赖的key
            depend_key = self.data.get_depend_field(i)
            #将依赖case的响应返回中某个字段的value赋值给该接口请求中某个参数
            data[depend_key] = depend_response_data

(4)拿到请求相关数据后,执行该条case,获取response;然后实际结果与预期结果进行断言

res = self.run_method.run_main(method,url,data,header,params=data)
'''
get请求参数是params:request.get(url='',params={}),post请求数据是data:request.post(url='',data={})
excel文件中没有区分直接用请求数据表示,则data = self.data.get_data_value(i)拿到的数据,post请求就是data=data,get请就是params=data
'''

根据get、post类型区分

class RunMethod:
    def post_main(self,url,data,header=None):
        res = None
        if header != None:
            res = requests.post(url=url,data=data,headers=header)
        else:
            res = requests.post(url=url,data=data)
        return res.json()

    def get_main(self,url,params=None,header=None):
        res = None
        if header != None:
            res = requests.get(url=url, params=params, headers=header)
        else:
            res = requests.get(url=url, params=params)
        return res.json()

    def run_main(self,method,url,data=None,header=None,params=None):
        res = None
        if method == 'post':
            res = self.post_main(url,data,header)
        else:
            res = self.get_main(url,params,header)
        return res

(5)执行接口case过程中,可能存在某条case异常报错,导致下面的case无法运行,所以我们既要将异常日志存放在特定文件中方便后续排查,也要保证下面的case能够不受影响继续执行完

           try:...
            
           except Exception as e:
                # 将报错写入指定路径的日志文件里
                with open(log_file,'a',encoding='utf-8') as f:
                    f.write("\n第%s条用例报错:\n" % i)
                initLogging(log_file,e)
                fail_count.append(i)

抓取日志的方法可以使用python内置模块logging,具体用法可以参考:https://www.cnblogs.com/shapeL/p/9174303.html

import logging

def initLogging(logFilename,e):

  logging.basicConfig(
                    level = logging.INFO,
                    format ='%(asctime)s-%(levelname)s-%(message)s',
                    datefmt = '%y-%m-%d %H:%M',
                    filename = logFilename,
                    filemode = 'a')
  fh = logging.FileHandler(logFilename,encoding='utf-8')
  logging.getLogger().addHandler(fh)
  log = logging.exception(e)
  return log

日志文件log.txt结果:直接定位问题出在哪儿

第5条用例报错:
18-06-19 10:27-ERROR-string indices must be integers
Traceback (most recent call last):
  File "C:/Users/xxx/Documents/GitHub/python3_interface/main/run_test.py", line 70, in go_on_run
    op_header.write_cookie()
  File "C:\Users\xxx\Documents\GitHub\python3_interface\util\operation_header.py", line 30, in write_cookie
    cookie = requests.utils.dict_from_cookiejar(self.get_cookie())
  File "C:\Users\zhangying1\Documents\GitHub\python3_interface\util\operation_header.py", line 25, in get_cookie
    url = self.get_response_url()+"&callback=jQuery21008240514814031887_1508666806688&_=1508666806689"
  File "C:\Users\xxx\Documents\GitHub\python3_interface\util\operation_header.py", line 18, in get_response_url
    url = self.response['data']['url'][0]
TypeError: string indices must be integers

(6)接口自动化测试执行完成后,需要将测试结果发送给项目组相关人员,邮件发送实现方法参考:https://www.cnblogs.com/shapeL/p/9115887.html

self.send_mail.send_main(pass_count,fail_count,no_run_count)
#coding:utf-8
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import datetime

class SendEmail:
    global send_user
    global email_host
    global password
    password = "lunkbrgwqxhfjgxx"
    email_host = "smtp.qq.com"
    send_user = "xxx@qq.com"

    def send_mail(self,user_list,sub,content):
        user = "shape" + "<" + send_user + ">"

        # 创建一个带附件的实例
        message = MIMEMultipart()
        message['Subject'] = sub
        message['From'] = user
        message['To'] = ";".join(user_list)

        # 邮件正文内容
        message.attach(MIMEText(content, 'plain', 'utf-8'))

        # 构造附件(附件为txt格式的文本)
        filename = '../log/log.txt'
        time = datetime.date.today()
        att = MIMEText(open(filename, 'rb').read(), 'base64', 'utf-8')
        att["Content-Type"] = 'application/octet-stream'
        att["Content-Disposition"] = 'attachment; filename="%s_Log.txt"'% time
        message.attach(att)

        server = smtplib.SMTP_SSL()
        server.connect(email_host,465)# 启用SSL发信, 端口一般是465
        # server.set_debuglevel(1)# 打印出和SMTP服务器交互的所有信息
        server.login(send_user,password)
        server.sendmail(user,user_list,message.as_string())
        server.close()

    def send_main(self,pass_list,fail_list,no_run_list):
        pass_num = len(pass_list)
        fail_num = len(fail_list)
        #未执行的用例
        no_run_num = len(no_run_list)
        count_num = pass_num + fail_num + no_run_num

        #成功率、失败率
        '''
        用%对字符串进行格式化
        %d 格式化整数
        %f 格式化小数;想保留两位小数,需要在f前面加上条件:%.2f;用%%来表示一个%
        如果你不太确定应该用什么,%s永远起作用,它会把任何数据类型转换为字符串 
       '''
        pass_result = "%.2f%%" % (pass_num/count_num*100)
        fail_result = "%.2f%%" % (fail_num/count_num*100)
        no_run_result = "%.2f%%" % (no_run_num/count_num*100)

        user_list = ['xxx@qq.com']
        sub = "接口自动化测试报告"
        content = "接口自动化测试结果:\n通过个数%s个,失败个数%s个,未执行个数%s个:通过率为%s,失败率为%s,未执行率为%s\n日志见附件" % (pass_num,fail_num,no_run_num,pass_result,fail_result,no_run_result)
        self.send_mail(user_list,sub,content)

到此,就基本完成。

 

说明:

1.只是大概整理了接口自动化实现的设计流程,需要源码参考的可查看:https://github.com/zhangying123456/python3_interface

2.这套接口框架中还有很多需要完善的地方,比如断言方法不够丰富,比如测试报告展示需要完善,等等。各位有兴趣的可以不断完善改进

 

]]>
MongoDb进阶实践之九 Mongodb的备份与还原 http://doc.okbase.net/PatrickLiu/archive/298938.html 可均可可 2018/6/19 11:10:20

一、引言

      前几天写了MongoDB数据库的聚合。一说到“聚合”,用过关系型数据库的人都应该知道它是一个什么东西,主要是用于对数据分类汇总和统计。大家都知道,做为DBA还有另一个重要的任务,那就是对数据库进行备份,以备当数据库发生损坏的时候,我们可以还原到以前的某个时刻,防止数据的丢失。今天我就来抛砖引玉,简单的说一说MongoDB文档数据库中的“备份-还原”的概念。

二、简介

      说起来数据库的“备份-还原”,在RDBMS系统中,都有很好的支持,也有很多选项可以设置,功能强大,也能自动完成大部分的备份功能,只要当初设置好了就可以了。对于MongoDB文档型的数据库来说,情况有一些不一样。在MongoDB中,要想对数据进行备份操作,需要使用脚本来执行命令完成,还原的的工作也是一样的,这是它本身支持的“备份-还原”的工作,相对RDBMS系统来说,要简单很多,不能自动完成。真的不能进行设置,来自动完成“备份和还原”的操作吗?当然可以,只是我们需要使用第三的软件才可以,而且功能越强大的,收费有的就越多,免费的也有,功能一般了,但是完成“备份-还原”的工作没问题。

          接下来我们就开始说说MongoDB自己的、原生态的“备份-还原”是如何实现的,脚本很简单。mongodump命名主要完成数据的备份工作,mongorestore命令主要完成数据库的还原操作。就这两个命名,是不是很简单,那就开始我们今天的正文吧。

三、数据库的备份和还原

    1、MongoDB数据库的备份

        1.1、mongodump命令脚本语法如下:

             >./mongodump -h dbhost -d dbname -o dbdirectory

            -h:--host【--host <hostname><:port>, -h <hostname><:port>】

               MongoDB所在服务器地址,默认地址:localhost(127.0.0.1),当然也可以指定端口号:localhost:27017


            -d:--db【--db <database>, -d <database>】

               需要备份的数据库实例,例如:school,这个数据库是我自己建立的,所以是该备份的


            -o:--out <path>【 --out <path>, -o <path>】

               备份的数据存放位置,例如:./databaseBack,当然该目录需要提前建立,在备份完成后,系统自动在dump目录下建立一个test目录,这个目录里面存放该数据库实例的备份数据。


          -u:--username <username>【--username <username>, -u <username>】

               指定用于向使用认证的MongoDB数据库认证的用户名,与--password和--authenticationDatabase结合使用


           -p:--password <password>【--password <password>, -p <password>】

               指定用于向使用认证的MongoDB数据库认证的密码。与--username和 --authenticationDatabase选项结合使用。


           -c:--collection <collection>【--collection <collection>, -c <collection>】

               指定要备份的集合。如果不指定,则会将指定数据库或实例中的所有集合备份。


           --port <port>:

               指定MongoDB实例监听客户连接的TCP端口号。端口的默认值:27017


           --gzip:

                MongoDB3.2或者以上的版本,可以进行压缩输出,如果mongodump指定导出到目录,则该选项会将每个文件都压缩,并添加.gz后缀;如果mongodump指定导出到文档或标准输出流,则该选项会压缩到文档或输出流中


           --authenticationDatabase:【--authenticationDatabase <dbname>】

                 如果您未指定身份验证数据库,则mongodump会假定指定要导出的数据库保存用户的凭据。如果您未指定要导出的身份验证数据库或数据库,则mongodump假定admin数据库保存用户的凭据。


           --authenticationMechanism:【 --authenticationMechanism <name>】

                默认值: SCRAM-SHA-1


        1.2、实例代码


          1】、登陆MongoDB的客户端,查看school数据库当前的详情

            //登陆MongoDB的客户端
            [root@linux bin]# ./mongo --host=192.168.127.130 --port=27017
            MongoDB shell version v3.6.3
            connecting to: mongodb://192.168.127.130:27017/
            MongoDB server version: 3.6.3
            Server has startup warnings:
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] ** WARNING: You are running this process as the root user, which is not recommended.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'.
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten] **        We suggest setting it to 'never'
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten]


            //显示当前数据库列表
            > show dbs
            admin   0.000GB
            config  0.000GB
            local   0.000GB
            school  0.000GB   //---这个数据库就是我们要备份的

            //切换到school数据库
            > use school
            switched to db school

            //显示当前school数据库下有多个集合
            > show tables
            geoInstance
            users

            //当前两个集合都有数据
            > db.users.find()
            { "_id" : ObjectId("5b1e044733091e826f7c2c74"), "title" : "MongoDb Overview", "description" : "Mongodb is not sql database", "author" : "huangFeiHong", "url" : "http://www.huangfeihong.com", "tags" : [ "mongodb", "database", "NoSql" ], "likes" : 100, "quantity" : 4 }
            { "_id" : ObjectId("5b1e044733091e826f7c2c75"), "title" : "NoSql Overview", "description" : "No sql database is very fast", "author" : "huangFeiHong", "url" : "http://www.huangfeihong.com", "tags" : [ "mongodb", "database", "NoSql" ], "likes" : 10, "quantity" : 12 }
            { "_id" : ObjectId("5b1e044733091e826f7c2c76"), "title" : "Log4Net Overview", "description" : "log4net is not sql database", "author" : "linChong", "url" : "http://www.linchong.com", "tags" : [ "log", "net", "NoSQL" ], "likes" : 750, "quantity" : 6 }
            { "_id" : ObjectId("5b1e1bfb33091e826f7c2c77"), "title" : "MongoDb Higher", "description" : "Mongodb is not sql database", "author" : "huangFeiHong", "url" : "http://www.huangfeihong.com", "tags" : [ "mongodb", "database", "NoSql" ], "likes" : 120, "quantity" : 4 }
            { "_id" : ObjectId("5b1e1bfb33091e826f7c2c78"), "title" : "NoSql Redis Overview", "description" : "No sql database is very fast", "author" : "linChong", "url" : "http://www.linchong.com", "tags" : [ "redis", "database", "NoSql" ], "likes" : 30, "quantity" : 33 }
            { "_id" : ObjectId("5b1e1bfb33091e826f7c2c79"), "title" : "Memcached Overrivew", "description" : "Memcached is sql database", "author" : "wuSong", "url" : "http://www.wusong.com", "tags" : [ "memcached", "cache", "NoSQL" ], "likes" : 50, "quantity" : 10 }
            >
            >
            > db.geoInstance.find()
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e03"), "loc" : [ 1, 3 ] }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e04"), "loc" : [ 3, 4 ] }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e05"), "loc" : [ 0, -3 ] }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e06"), "loc" : [ -6, 2 ] }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e07"), "loc" : { "x" : 9, "y" : 5 } }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e08"), "loc" : { "lng" : -9.2, "lat" : 21.3 } }


          2】、退出MongoDB的客户端,我们要创建保存备份文件的目录
       

            //退出MongoDB的客户端
            > exit
            bye
            [root@linux bin]#

            //当前目录结构
            [root@linux bin]# pwd
            /root/application/program/mongodb/mongodb-linux-x86_64-3.6.3/bin
            [root@linux bin]# databaseBack    //创建保存备份文件的目录名称

            
            //已经创建好的目录结构,databaseBack是备份目录
            [root@linux bin]# ls
            bsondump      datas            logs   mongod        mongodump    mongofiles   mongoperf    mongorestore  mongostat
            databaseBack  install_compass  mongo  mongodb.conf  mongoexport  mongoimport  mongoreplay  mongos        mongotop


          3】、开始备份数据库,因为我们使用的是客户端,要写完整的地址,在MongoDB服务器端就可以省略

            //代码很简单
            [root@linux bin]# ./mongodump -h 192.168.127.130:27017 -d school -o ./databaseBack
            2018-06-13T11:50:00.816+0800    writing school.geoInstance to
            2018-06-13T11:50:00.817+0800    writing school.users to
            2018-06-13T11:50:00.821+0800    done dumping school.geoInstance (6 documents)
            2018-06-13T11:50:00.821+0800    done dumping school.users (6 documents)


            //查看备份文件
            [root@linux bin]# ls ./databaseBack
            school
            [root@linux bin]# ls ./databaseBack/school
            geoInstance.bson  geoInstance.metadata.json  users.bson  users.metadata.json


          4】、以下操作将创建一个只包含名为school的数据库中名为users的集合的转储文件。 在这种情况下,数据库在端口27017上的192.168.127.130运行:

            [root@linux bin]# ./mongodump -h 192.168.127.130:27017 --db school --collection users -o ./backCollection
            2018-06-13T13:40:46.702+0800    writing school.users to
            2018-06-13T13:40:46.706+0800    done dumping school.users (6 documents)
            [root@linux bin]# ls ./backCollection
            school
            [root@linux bin]# ls ./backCollection/school
            users.bson  users.metadata.json
            [root@linux bin]# 

 


          5】、要将转储输出到存档文件,请使用--archive选项和存档文件名运行mongodump。 例如,以下操作将创建一个包含school数据库转储的文件school.20180613.archive。

            [root@linux bin]# ./mongodump -h 192.168.127.130:27017 --archive=./backArchiveFile/school.2018613.archive --db school
            2018-06-13T13:57:51.953+0800    writing school.geoInstance to archive './backArchiveFile/school.2018613.archive'
            2018-06-13T13:57:51.955+0800    writing school.users to archive './backArchiveFile/school.2018613.archive'
            2018-06-13T13:57:51.994+0800    done dumping school.geoInstance (6 documents)
            2018-06-13T13:57:51.997+0800    done dumping school.users (6 documents)
            [root@linux bin]# ls ./backArchiveFile/
            school.2018613.archive


          6】、要压缩输出转储目录中的文件,请使用新的--gzip选项运行mongodump。 例如,以下操作将压缩文件输出到默认转储目录。

            [root@linux bin]# ./mongodump -h 192.168.127.130:27017 --db school --gzip -o ./backZip
            2018-06-13T14:04:39.674+0800    writing school.geoInstance to
            2018-06-13T14:04:39.675+0800    writing school.users to
            2018-06-13T14:04:39.682+0800    done dumping school.geoInstance (6 documents)
            2018-06-13T14:04:39.684+0800    done dumping school.users (6 documents)
        
            //显示压缩文件
            [root@linux bin]# ls ./backZip/school
            geoInstance.bson.gz  geoInstance.metadata.json.gz  users.bson.gz  users.metadata.json.gz


          7】、想要通过mongodump命令将压缩的文件输出到指定目录,请将--gzip选项与--archive选项一起使用,并指定压缩文件的名称。

            [root@linux bin]# ./mongodump -h 192.168.127.130:27017 --archive=./backZip/school.2018613.gz --db school --gzip
            2018-06-13T14:08:57.311+0800    writing school.geoInstance to archive './backZip/school.2018613.gz'
            2018-06-13T14:08:57.312+0800    writing school.users to archive './backZip/school.2018613.gz'
            2018-06-13T14:08:57.346+0800    done dumping school.geoInstance (6 documents)
            2018-06-13T14:08:57.349+0800    done dumping school.users (6 documents)

            [root@linux bin]# ls ./backZip/
            school.2018613.gz



    2、MongoDB数据库的还原


          2.1、mongorestore命令脚本语法如下:

             >mongorestore -h <hostname><:port> -d dbname <path>

                  --host <:port>, -h <:port>:

                  MongoDB所在服务器地址,默认为: localhost:27017

                  --db , -d :

                  需要恢复的数据库实例,例如:school,当然这个名称也可以和备份时候的不一样,比如school3

                  --drop:

                  还原的时候,先删除当前的数据,然后还原备份的数据。也就是说,还原后,成功备份之后再添加或者修改的数据都会被删除,慎用哦!

                  <path>:

                  mongorestore 最后的一个参数,设置备份数据所在位置,如:./databaseBack

                  你不能同时指定 <path> 和 --dir 选项,--dir也可以设置备份目录。

                  --dir:

                  指定备份数据的目录地址

                  你不能同时指定 <path> 和 --dir 选项。


        2.2、示例代码

          1】、上文已经成功备份了数据库,现在我们要登陆MongoDB客户端把我们的school数据库删除。

            //登陆MongoDB客户端,显示当前数据库列表
            [root@linux bin]# ./mongo --host=192.168.127.130 --port=27017
            MongoDB shell version v3.6.3
            connecting to: mongodb://192.168.127.130:27017/
            MongoDB server version: 3.6.3
            Server has startup warnings:
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] ** WARNING: You are running this process as the root user, which is not recommended.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'.
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten] **        We suggest setting it to 'never'
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten]
            > show dbs
            admin   0.000GB
            config  0.000GB
            local   0.000GB
            school  0.000GB
            >

            //删除school数据库
            > use school
            switched to db school
            > db.dropDatabase()
            { "dropped" : "school", "ok" : 1 }
            >
            >
            > show dbs
            admin   0.000GB
            config  0.000GB
            local   0.000GB
            >

            //退出MongoDB客户端
            > exit
            bye
            [root@linux bin]# 

 


          2】、我们开始还原我们的school数据库吧,我们从集合来恢复数据库。

            //还原users集合
            [root@linux bin]# ./mongorestore -h 192.168.127.130:27017 --collection users --db school ./databaseBack/school/users.bson
            2018-06-13T13:16:52.118+0800    checking for collection data in databaseBack/school/users.bson
            2018-06-13T13:16:52.120+0800    reading metadata for school.users from databaseBack/school/users.metadata.json
            2018-06-13T13:16:52.148+0800    restoring school.users from databaseBack/school/users.bson
            2018-06-13T13:16:52.218+0800    no indexes to restore
            2018-06-13T13:16:52.218+0800    finished restoring school.users (6 documents)
            2018-06-13T13:16:52.219+0800    done


            //还原geoInstance集合
            [root@linux bin]# ./mongorestore -h 192.168.127.130:27017 --collection geoInstance --db school ./databaseBack/school/geoInstance.bson
            2018-06-13T13:18:26.353+0800    checking for collection data in databaseBack/school/geoInstance.bson
            2018-06-13T13:18:26.358+0800    reading metadata for school.geoInstance from databaseBack/school/geoInstance.metadata.json
            2018-06-13T13:18:26.386+0800    restoring school.geoInstance from databaseBack/school/geoInstance.bson
            2018-06-13T13:18:26.456+0800    restoring indexes for collection school.geoInstance from metadata
            2018-06-13T13:18:26.475+0800    finished restoring school.geoInstance (6 documents)
            2018-06-13T13:18:26.475+0800    done


          3】、查看一下我们还原的数据。       

            [root@linux bin]# ./mongo --host=192.168.127.130 --port=27017
            MongoDB shell version v3.6.3
            connecting to: mongodb://192.168.127.130:27017/
            MongoDB server version: 3.6.3
            Server has startup warnings:
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] ** WARNING: You are running this process as the root user, which is not recommended.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'.
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten] **        We suggest setting it to 'never'
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten]
            >
            >
            > show dbs
            admin   0.000GB
            config  0.000GB
            local   0.000GB
            school  0.000GB
            >
            >
            > use school
            switched to db school
            >
            > show tables
            geoInstance
            users
            >
            >
            > db.users.find()
            { "_id" : ObjectId("5b1e044733091e826f7c2c74"), "title" : "MongoDb Overview", "description" : "Mongodb is not sql database", "author" : "huangFeiHong", "url" : "http://www.huangfeihong.com", "tags" : [ "mongodb", "database", "NoSql" ], "likes" : 100, "quantity" : 4 }
            { "_id" : ObjectId("5b1e044733091e826f7c2c75"), "title" : "NoSql Overview", "description" : "No sql database is very fast", "author" : "huangFeiHong", "url" : "http://www.huangfeihong.com", "tags" : [ "mongodb", "database", "NoSql" ], "likes" : 10, "quantity" : 12 }
            { "_id" : ObjectId("5b1e044733091e826f7c2c76"), "title" : "Log4Net Overview", "description" : "log4net is not sql database", "author" : "linChong", "url" : "http://www.linchong.com", "tags" : [ "log", "net", "NoSQL" ], "likes" : 750, "quantity" : 6 }
            { "_id" : ObjectId("5b1e1bfb33091e826f7c2c77"), "title" : "MongoDb Higher", "description" : "Mongodb is not sql database", "author" : "huangFeiHong", "url" : "http://www.huangfeihong.com", "tags" : [ "mongodb", "database", "NoSql" ], "likes" : 120, "quantity" : 4 }
            { "_id" : ObjectId("5b1e1bfb33091e826f7c2c78"), "title" : "NoSql Redis Overview", "description" : "No sql database is very fast", "author" : "linChong", "url" : "http://www.linchong.com", "tags" : [ "redis", "database", "NoSql" ], "likes" : 30, "quantity" : 33 }
            { "_id" : ObjectId("5b1e1bfb33091e826f7c2c79"), "title" : "Memcached Overrivew", "description" : "Memcached is sql database", "author" : "wuSong", "url" : "http://www.wusong.com", "tags" : [ "memcached", "cache", "NoSQL" ], "likes" : 50, "quantity" : 10 }
            >
            >
            > db.geoInstance.find()
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e03"), "loc" : [ 1, 3 ] }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e04"), "loc" : [ 3, 4 ] }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e05"), "loc" : [ 0, -3 ] }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e06"), "loc" : [ -6, 2 ] }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e07"), "loc" : { "x" : 9, "y" : 5 } }
            { "_id" : ObjectId("5b1f4d21cf30d7cba03d5e08"), "loc" : { "lng" : -9.2, "lat" : 21.3 } }

 


          4】、我们可以使用nsInclude参数来恢复指定集合的数据。

            [root@linux bin]# ./mongorestore -h 192.168.127.130:27017 --nsInclude school.users ./databaseBack/
            2018-06-13T14:19:59.942+0800    preparing collections to restore from
            2018-06-13T14:19:59.946+0800    reading metadata for school.users from databaseBack/school/users.metadata.json
            2018-06-13T14:19:59.970+0800    restoring school.users from databaseBack/school/users.bson
            2018-06-13T14:19:59.977+0800    no indexes to restore
            2018-06-13T14:19:59.977+0800    finished restoring school.users (6 documents)
            2018-06-13T14:19:59.977+0800    done


            //这是在有数据的情况下执行恢复
            [root@linux bin]# ./mongorestore -h 192.168.127.130:27017 --nsInclude school.users ./databaseBack/
            2018-06-13T14:21:10.743+0800    preparing collections to restore from
            2018-06-13T14:21:10.747+0800    reading metadata for school.users from databaseBack/school/users.metadata.json
            2018-06-13T14:21:10.748+0800    restoring school.users from databaseBack/school/users.bson
            2018-06-13T14:21:10.755+0800    error: multiple errors in bulk operation:
              - E11000 duplicate key error collection: school.users index: _id_ dup key: { : ObjectId('5b1e044733091e826f7c2c74') }
              - E11000 duplicate key error collection: school.users index: _id_ dup key: { : ObjectId('5b1e044733091e826f7c2c75') }
              - E11000 duplicate key error collection: school.users index: _id_ dup key: { : ObjectId('5b1e044733091e826f7c2c76') }
              - E11000 duplicate key error collection: school.users index: _id_ dup key: { : ObjectId('5b1e1bfb33091e826f7c2c77') }
              - E11000 duplicate key error collection: school.users index: _id_ dup key: { : ObjectId('5b1e1bfb33091e826f7c2c78') }
              - E11000 duplicate key error collection: school.users index: _id_ dup key: { : ObjectId('5b1e1bfb33091e826f7c2c79') }

            2018-06-13T14:21:10.755+0800    no indexes to restore
            2018-06-13T14:21:10.755+0800    finished restoring school.users (6 documents)
            2018-06-13T14:21:10.755+0800    done

 


          5】、我们可以使用zip压缩文件,通过archive参数来恢复指定数据库的数据。

            [root@linux bin]# ./mongorestore -h 192.168.127.130:27017 --gzip --db school  --archive=./backZip/school.2018613.gz
            2018-06-13T14:26:48.939+0800    the --db and --collection args should only be used when restoring from a BSON file. Other uses are             deprecated and will not exist in the future; use --nsInclude instead
            2018-06-13T14:26:49.591+0800    preparing collections to restore from
            2018-06-13T14:26:50.041+0800    reading metadata for school.geoInstance from archive './backZip/school.2018613.gz'
            2018-06-13T14:26:50.071+0800    restoring school.geoInstance from archive './backZip/school.2018613.gz'
            2018-06-13T14:26:51.132+0800    restoring indexes for collection school.geoInstance from metadata
            2018-06-13T14:26:51.150+0800    finished restoring school.geoInstance (6 documents)
            2018-06-13T14:26:51.152+0800    reading metadata for school.users from archive './backZip/school.2018613.gz'
            2018-06-13T14:26:51.181+0800    restoring school.users from archive './backZip/school.2018613.gz'
            2018-06-13T14:26:51.837+0800    no indexes to restore
            2018-06-13T14:26:51.838+0800    finished restoring school.users (6 documents)
            2018-06-13T14:26:51.838+0800    done



            //以前已经删除了school数据库
            [root@linux bin]# ./mongo --host 192.168.127.130 --port=27017
            MongoDB shell version v3.6.3
            connecting to: mongodb://192.168.127.130:27017/
            MongoDB server version: 3.6.3
            Server has startup warnings:
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten] ** WARNING: You are running this process as the root user, which is not recommended.
            2018-06-13T11:27:09.721+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten]
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'.
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten] **        We suggest setting it to 'never'
            2018-06-13T11:27:09.723+0800 I CONTROL  [initandlisten]
            > show dbs
            admin   0.000GB
            config  0.000GB
            local   0.000GB
            school  0.000GB  //数据库已经存在了,说明恢复成功



五、结束

      好了,就是这些了。MongoDB的备份和还原还是有很多内容的,mongodump和mongorestore命令有很多参数可以去配置,今天只是一个初步认知,如果大家想查看更详细的内容,可以查看英文的网站。地址如下:https://docs.mongodb.com/manual/reference/program/mongodump/。不忘初心,继续努力吧。

]]>
We Do Sleep At Night, We Do REST Right http://doc.okbase.net/Wddpct/archive/298937.html 白细胞 2018/6/19 10:46:38

Github 同步发表链接

前言

笔者在上一篇文章中提过,任何一种非“强制性”约束同时也没有“标杆”工具支持的开发风格或协议,最后都会在不同的程序员手中得到不同的诠释,微服务是如此,DDD 是如此,笔者把它称为技术思想上的“康威定律”。不出意外的,REST 同样难逃此劫。光是在学习和收集资料的过程中,笔者就已经见过不下十多篇此类理解,甚至于在 url 中使用短划线或下划线连接单词也是众口难调。

尽管这只是小事。

微软也发布过关于如何设计 REST API 的开发指南,但是不幸的是,REST 的创始人 Roy Fielding 认为微软的 REST API 规范与 REST 没有多大关系。

“即使是我最糟糕的 REST 描述也比微软的 API 指南提供的总结或参考要好很多。”

那什么才是正确的 REST 描述呢,或者说,REST 是什么。本文的创作动机便是希冀于解决这样一个问题。

本文假设读者已经具备基本的 REST 和 Web 知识,哪怕你们现在认为 HTTP API 就是 REST API 也可。

REST 起源

REST 英文全称为 Representational State Transfer,又名“表述性状态移交”,是由 Roy Fielding 在《架构风格与基于网络的软件架构设计》一文中提出的一种架构风格(Architectural Style)。而在这篇 REST 圣经问世之前,R.F 博士就已经参与了 HTTP 1.0 协议规范的开发工作(1996年),并且负责了 HTTP 1.1 协议规范的制定(1997年)。

一种架构风格由一组准确命名的,相互协作的架构约束组成。当我们在谈论 REST 本质的时候,我们谈论的其实是架构约束。

REST 用以指导基于网络的分布式超媒体系统的设计和实现,Web(即万维网)就是一种典型的分布式超媒体系统。可以确定的是,在制定 HTTP 协议的过程中,R.F 博士就已经以 REST 架构风格作为指导原则来完成相关工作。论文中提到了以下内容:

“在过去的6年中,我们使用 REST 架构风格来指导现代 Web 架构的设计和开发。这个工作是与我所创作的 HTTP 和 URI 两个互联网规范共同完成的,这两个规范定义了在 Web 上进行交互的所有组件所使用的通用接口。”

“自从1994年起,REST 架构风格就被用来指导现代 Web 架构的设计与开发。”

“开发 REST 的动机是为 Web 的运转方式创建一种架构模型,使之成为 Web 协议标准的指导框架。”

“REST 的第一版开发于1994年10月至1995年8月之间,起初,在我编写 HTTP/1.0 规范和最初的 HTTP/1.1 建议时,将它用来作为表达各种 Web 概念的一种方法……”

Web 架构规范主要包括 HTTP, URI 和 HTML 等。

所以我们也不难理解为什么 REST 与 Web 和 HTTP 能够结合得如此紧密。尽管直到2000年,这只“鸡”才在下完鸡蛋后,出现在了世人面前。

REST 演化

REST 约束

无论是否愿意承认,REST 一开始就是为 Web 而服务的,可以这么说的是,REST 是现代 Web 的架构风格,Web 也是 REST 最典型和最成功的案例。包括在 R.F 博士的论文中,他也是在解决现代 Web 需求(无法控制的可伸缩性和独立部署)的过程中而逐步推导出 REST。前文已经提到一种架构风格是由一组准确命名的,相互协作的架构约束组成。而所谓架构约束,便是这个推导过程中最重要的产物。甚至高于 REST 本身。

早先的 Web 与 REST 所描述的模型有着大量出入,然而正是在对应的 HTTP 和 URI 规范出炉后,才有了所谓“现代 Web”的说法。笔者更愿意把“现代 Web”的定义期限定为1996年后。

客户端 - 服务端

设计与实现上的关注点分离。

无状态

在客户端没有发起请求时,服务器并不知道它的存在。同样的,服务器无须维护当前请求之外的客户端状态,从而改善服务器的可伸缩性。Session 和 Cookie 都是“需要”被抛弃的。
如果有些应用状态重要到服务器需要去关心,那它应该成为一个资源。

缓存

对于客户端而言,使用缓存则是维护状态和提升性能的更好做法。

统一接口

使 REST 架构风格区别于其他基于网络的架构风格的核心特征是,它强调组件之间具有一个统一的接口。实现与他们所提供的服务是解耦的,这促进了独立的可进化性。同时这也引申出了其他的约束:资源识别;通过表述来操作资源;自描述信息;超媒体作为应用状态引擎(即 HATEOAS)。下文会专门说明。

分层系统

“分层系统”约束在“客户 - 服务端”约束的基础上增加了代理组件和网关组件。尽管笔者认为代理和网关都不是重点,“分层系统”约束更注重的是“在客户端和服务端之间添加一个组件应该是一个透明操作”,组件只能“看到”与其交互的相邻层(是不是想到了迪米特法则),使用层级来封装服务,同时能够支持负载均衡和诸如安全性检查的功能。

按需代码

这是六大约束中唯一的可选约束。REST 允许客户端通过下载并执行脚本或其他形式的代码,对客户端的功能进行扩展,从而提高客户端的灵活性和性能。通俗点说,HTML 中的 <script> 标签就是一种按需代码,尽管它可能会导致一些例如跨站脚本攻击这样的问题。

统一接口约束

R.F 博士在论文中针对六大约束中的“统一接口”做了额外的约束分解和说明,但遗憾的是并没有以列表的方式展示出来。但在接下来的内容中你可能就会发现,这几项可能是目前大部分开发者践行 REST 原则时所遵循的全部标准。

资源识别

REST 对于信息的核心抽象是资源,任何能够被命名的信息都可以称为是资源,只要你的想象力允许。资源一词通常和“可寻址性”绑定,一个或多个 URI 标识一个资源。如果资源的 URI 发生了变化,服务器应该使用超媒体引导客户端访问新的 URI 或提示对应信息。

通过表述来操作资源

当客户端对一个资源发起一个请求时,服务器会以一种有效的方式提供一个采集了资源信息的文档作为回应。这就是表述——一种以机器可读的方式对资源当前状态的说明。客户端和服务器之间也可以继续传递表述,从而对资源执行某种操作。客户端从来不会直接看到资源,能看到的都是资源的表述。可以这么说的是,服务器发送的表述用于描述资源当前的状态,客户端发送的表述用于描述客户端希望资源拥有的状态,这就是表述性状态转移/移交。

一个表述由一个“字节序列”和描述这些字节的“表述元数据”构成,且不与服务器端代码绑定,这意味着当服务器端的资源实现和业务操作代码发生变化时,可以选择不更改资源的呈现方式。

值得注意的是,一般人通常会将表述认为成资源的“值”,这虽然可以理解,但是当你请求一个天气服务时,千万不要认为表述一定便是温度等确定的值信息,因为它仍然可能是某次响应中的错误提示。一个表述的具体含义取决于消息中的控制数据。

“控制数据定义了在组件之间移交的消息的用途,例如被请求的动作或相应的含义。它也可用于提供请求的参数,或覆盖某些连接元素的默认行为。例如,可以使用(包含在请求或响应消息中的)控制数据来修改缓存的行为。”

“表述的数据格式称为媒体类型(media type)。发送者能够将一个表述包含在一个消息中,发送给接收者。接收者收到消息之后,根据消息中的控制数据和媒体类型的性质,来对该消息进行处理。”

表述在现代 Web 中的实例包括 HTML,Json,XML,图片等。

自描述的消息

一个 (HTTP)消息体包含了所有足以让接收者理解它的必要信息,在现代 Web 中,自描述的消息由一些标准的HTTP方法、可定制的HTTP头信息、可定制的HTTP响应代码组成。扩展开来,它通常有以下三方面的含义。

  1. 请求之间的交互是无状态的。 对应于 REST 约束中的“无状态”约束,服务器可以独立处理每个请求,而无须对该客户端先前所有请求的处理进行记忆。
  2. 使用标准的方法和媒体类型来表达语义和交换信息。想想 HTTP Methods 和 HTTP Headers,客户端通常靠这些信息理解请求的含义和解析消息体。
  3. 响应可以明确地表明其可缓存性。

超媒体作为应用状态引擎

该约束便是大名鼎鼎的“HATEOAS”(Hypertext/Hypermedia As The Engine Of Application State),但实际上 R.F 博士在论文中并没有对它做过详细的介绍。在目前的共识中(讽刺的是在大多数时候它并没有被应用到设计所谓 REST APIs 中去),HATEOAS 意味着客户端应该使用超文本来作为你在接收到当前的表述后,再进行下一步寻址的方式。更进一步的,客户端需要通过解析超文本理解服务器提供了哪些资源,而不是在客户端事先定义或约定俗成。

“客户端依赖的是超文本的状态迁移语义,而不应该对于是否存在某个URI或URI的某种特殊构造方式作出假设。一切都有可能变化,只有超媒体的状态迁移语义能够长期保持稳定。” —— 《理解本真的REST架构风格》

最终结果便是客户端可以自动化地适应服务器端的变化,服务器也允许在不破坏所有客户端的情况下更改它底层的实现。同样的,我们可以列出几点说明。

  1. 所有的应用状态维持在客户端一侧。改变应用状态是客户端的职责。
  2. 客户端仅能够通过发送请求和处理响应来改变应用状态。
  3. 客户端可以通过已经收到的表述中的超文本知道接下来可以操作的请求动作(如 HTML 中的超链接)。
  4. 超文本是应用状态变化背后的动力。

看起来,上述四点内容说的多是集中式 Web 应用的情况,在如今多用 Web APIs 进行前后端分离开发的 Web 应用中,HATEOAS 又该做如何理解呢?如今有这么一项技术可以让超文本继续充当驱动应用状态更新流动的引擎,那就是 Web Links,RFC 5988 定义了 HTTP 的这项扩展。

Github REST API v3 中,我们可以在很多 apis (如列表翻页)的响应体中看到 Link Header,对应引导的 Uri 同样有相关标准,即 Uri Templates(RFC 6570)

Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
  <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

至此,想必你也大致清晰了 HATEOAS 的含义。

如果可以的话,你可以将“应用状态”理解为客户端对资源操作后的展示结果。比如“主页”,“博客”,“关于我”,“成功提交”等操作界面。它和“资源状态”有抽象概念上的区分。


你可以放弃对 Hypertext 和 Hypermedia 之间区别的思考,笔者也认为它们在你理解 REST 时并不应该区分。

Richardson 成熟度模型

Richardson Maturity Model 是一个可以尝试的甜品,特别是当你在设计自己的 REST APIs 时。和 REST 相似,你也可以把该模型称为指导原则。

Richardson Maturity Model

笔者无意去解释这几个层级,因为这些层级和“统一接口”的扩展约束都是间接的映射关系。而且正如上文一直在强调的,REST 不依赖于任何单一的通信/传输/移交协议,所以模型中的 HTTP 指示就有些耐人寻味了。

“它是标准吗?”
“当然不是,它只是目前设计 REST APIs 时的一种潜规则。”

总结

对于理解概念性文章的总结总是特别艰难,看起来内容挺丰富,同时结合了 R.F 博士论文及其译者李琨教授相关文章,其实摊开了目录整篇文章也就只涉及到了起源—>约束->模型这样几个方面,实际上 REST 也确实只是一组约束而已。最后,仅用笔者认为的 R.F 博士论文中至关重要的两段话作为结束。

“因此,REST的模型应用是一个引擎,它通过检查和选择当前的表述集合中的状态迁移选项,从一个状态迁移到下一个状态。毫不奇怪,这与一个超媒体浏览器的用户接口完全匹配。然而,REST风格并不假设所有应用都是浏览器。事实上,通用的连接器接口对服务器隐藏了应用的细节,因此各种形式的用户代理都是等价的,无论是为一个索引服务执行信息获取任务的自动化机器人,还是查找匹配特定查询标准的数据的私人代理,或者是忙于巡视破损的引用或被修改的内容的维护爬虫。”

“这个名称“表述性状态转移”是有意唤起人们对于一个良好设计的Web应用如何运转的印象:一个由网页组成的网络(一个虚拟状态机),用户通过选择链接(状态迁移)在应用中前进,导致下一个页面(代表应用的下一个状态)被转移给用户,并且呈现给他们,以便他们来使用。”

有效的参考文档

  1. 理解本真的REST架构风格
  2. 理解本质的 REST
  3. REST APIs must be hypertext-driven
  4. HATEOAS作为领域特定协议描述的引擎
  5. Richardson Maturity Model
]]>
金牌架构师:我们是这样设计APP数据统计产品的 http://doc.okbase.net/evakang/archive/298936.html eva7 2018/6/19 10:23:19

作者董霖,个推高级技术总监。

前言:近期,智能大数据服务商“个推”推出了应用统计产品“个数”,今天我们就和大家来谈一谈个数实时统计与AI数据智能平台整合架构设计。

很多人可能好奇,拥有数百亿SDK的个推,专注消息推送服务多年,现在为什么要做应用统计?毕竟市面上已经有非常多的类似产品了。我认为答案是“天时地利人和”。

首先是天时。目前,互联网行业已发展到了所谓的“下半场”甚至是“加时赛”,运营工作走向精细化,DAU和效果被放到第一位,从业者们也逐步认识到数据优化及使用良好的模型的重要性。

其二是地利。个推经过多年的积累,拥有了坚实的数据基础;另外,个推基础架构也非常成熟,并在诸多垂直领域实实在在提供了很多数据服务。

第三是人和。内部的研发人员在实战中积累了丰富的经验,公司与外部应用开发者和合作伙伴建立了长期紧密的联系。

正是在这样的背景下,我们推出了这一款应用统计产品“个数”。

前段时间流行的词汇是“growth hacker”,而现阶段,单纯的用户增长已经无法满足发展,公司及产品的思考点都在于“效果”。相比于其他统计产品,个数产品的灵魂是运营,即围绕着核心KPI,保持应用的活跃度,提高整体的收益。

安全、准确、灵活的数据能够保证运营工作的有效开展;而承载数据的平台则需做到高并发、高可用、高实时;SDK作为基础,其核心在于包的体积足够小,并且集成方便,能够快速运行。这样一个从上到下的金字塔,构建起了个数产品。

四大核心能力,打造智能化统计

首先,实时的多维统计是整个应用统计的基础功能。其中,稳定与实时是两大关键;在颗粒度方面,页面级统计最适合运营者。

第二部分是数据整合。利用个推的大数据能力,我们能够提供独特的第三方视角,帮助应用认清自身,并找到它在行业内的地位。

第三部分是自动建模预测。这是个数非常独特的功能点。我们希望通过一整套解决方案,帮助应用开发者真正体验到模型的价值,并通过实际数据反馈,不断优化改进产品。

第四部分是精准推送。个推最广为人知的能力就是推送服务,而将应用内的统计数据与推送系统有效整合,能够辅助更加精细化的运营。

技术架构:业务域+数据域

个数的整体架构分为业务域与数据域。其中,数据域分为三个层面:数据网关层、数据业务层和数据平台层。

数据网关层主要做业务层与数据层之间的承载,包括Kafka集群与API网关,使得上下数据互通。数据业务层部分主要基于特定业务的研发工作,由于这部分工作不在平台间通用,因而是独立的一层。在这一层下,产品根据功能的不同配置了若干个独立的Hadoop集群,同时把核心能力包装成公共服务,提供给业务研发人员使用。

业务域部分包括了传统的微服务及相应的存储模块。

第一,这两层之间的数据防火墙非常重要,二级数据防火墙可确保系统内部数据的有效隔离。

第二,数据域的分层。对此,个数架构上设立的三层对应三个不同的职能团队,数据网管层—数据运维,数据业务层—业务线的研发团队,数据平台层—数据部门,这样的职能划分可以有效提升业务线产品研发效率。

第三,集群资源的隔离。业务线的开放集群需要通过资源划分的方式,实现资源的隔离。此外,隔离GPU计算集群资源也是非常有必要的。

第四,实时与离线的兼顾。在开发时,无论是何种产品,我们始终需要把实时和离线两种情况考虑在内。

最后,数据储存。业务线、数据层、平台层都要有相应的数据储存。此外,应通过合理的规划,确保每一类数据存放在合适的位置。

实时多维统计架构解析

Mobile API从SDK收集到上报的数据,以文件形式Log保存下来,通过Flume进入到Kafka,接下来通过实时与离线两条路进行处理,最后通过数据API封装提供给上层的业务系统使用。

在离线统计方面,个数可支持到小时级别。同时,我们会全流程监控数据的流转情况,当出现数据丢失或者延迟等情况时,确保第一时间监测到。

在这里需要补充几个关键的、需要解决的点:用户去重、页面的唯一性标识、多维度统计的处理策略,以及保证数据在各个环节中不丢失。

数据整合,提供多维指标

个推拥有强大的大数据能力,可以为应用统计产品提供丰富的数据维度。

首先,设备指纹。目前移动设备存在兼容性混乱等问题,个推则通过为应用打上唯一的设备ID标识来解决这个问题。

第二,以第三方视角提供应用留存、安装、卸载,活跃等中立的分析数据。

第三,用户画像。无论是性别、年龄段等静态标签,还是兴趣爱好等标签,都可通过个推的大数据平台获得。

自动建模预测&模型评估

一个标准化的建模工作大体包含以下几个步骤:首先选取一批正负样本用户;然后对其进行特征补全,把无关特征进行降维操作;之后,选择合适的模型进行训练,这也是一个非常消耗CPU的过程;接下来是目标预测,我们需要整理或补齐目标用户的所有特征,再将数据投入模型中,获得预测结果;最后是模型评估。模型评估之后,再进行下一个迭代调整,循环往复。

在建模环节,实时性是需要考虑的重要因素之一。最传统的离线训练是很常规的建模方式。预测可以选择高性能的离线方式,但它的缺点是反馈太慢,有可能导致结果出来之前没有其他的机会实施运营方案,因而我们需要提供更实时的预测功能。比如用户新安装或完成某个操作之后,系统实时获得预测结果,并立即进行运营干预。

最后是实时训练,从我个人的角度来看,这是未来发展的一个方向。

对于整个建模的基础架构,毫无疑问我们选择了tensorflow,目前主流的模型都可以在tensorflow下实现。它拥有诸多优点:支持分布式部署,可并发、集成扩展,可支撑集群Serving,能够以API形式提供模型服务……因而它非常适合预测服务的技术架构。

离线建模过程如下:数据落到HDFS之后,先通过Azkaban进行任务调度,数据清洗后把应用内的统计数据收集汇总,接下来将个推拥有的大数据能力与之进行整合,形成整体的数据Cube输入到TF集群,TF集群会根据预测事件的配置,综合进行模型训练,最后输出结果。

目标预测实现方案相对简单,只需要把模型导入到tensorflow的Serving集群即可。预测结果再通过DAPI封装出来,给到上层业务层调用。

目标预测首先要进行特征补全。这项工作极富挑战,需要针对每一个新用户的要求尽快预测并完美地补全特征。

第二部分是预测结果。预测最终得到的是概率值,我们需要去评估概率值是否处在合理范围内,概率分布是否符合我们的预期。如果不达标,我们就需要重新评估这个模型,或者认为预测是失效的。

第三部分是tensorflow集群。通过容器化部署,可以将预测服务部署到独立的Pod上。根据不同的实时性要求,个数可通过API的形式提供对外服务,也可以提供实时回调。

模型评估是预测的关键步骤,评价体系不完备可能直接导致最后的结果不可用。

精准率与召回率,这两个与预测准确度相关的基础指标是需要重点关注的。由于精确度与阈值相关,我们也支持开发者自主调整。

Lift也是一项重要指标,它反映了我们的预测能够产生多大的效果提升。显而易见,筛选的人群比例越大,提升的比例会逐渐递减。具体应用的时候,我们需要根据场景或需求来选择一个合理的值。

ROC与AOC,这两个指标作为模型整体评估指标,用于评估在不同阈值下模型的表现。为提升模型的区分能力,我们势必会追求AOC最大化。AOC值是一个定量的指标,适合做模型的持续监控。此外,对模型做每日评估也是必要的,如果AOC值不能够达到预期,我们可以及时选择其他模型。

在监控方面,首先要确保测试用户的选择足够随机。我们每天会选择一批测试用户来验证模型的效果,然后评估准确率、召回率以及AOC。除了内部校验,我们也会把这个指标提供给开发者。同时,缓存预测结果的历史数据,可以辅助每天的效果评估。

精准推送集成,增能实际场景

应用内埋点数据和预测结果可以通过个数传递到推送系统,方便开发者在推送环节直接以人群包的形式选择目标用户,或者下载这个人群包,上传到广点通等平台做广告投放。

个数Roadmap

个数产品在5月份已经正式对外开放,大家可以在自由注册并使用。模型预测功能目前处于测试阶段,我们希望到Q4时,能够正式把能力对外开放出来,帮助大家认识模型、使用模型,并享受模型带来的价值。

]]>
从App的角度看进程和线程 http://doc.okbase.net/qindongliang1922/archive/298935.html qindongliang1922 2018/6/19 10:04:57 原创发自我的公众号:我是攻城师

https://mp.weixin.qq.com/s?__biz=MzAxMzE4MDI0NQ==&mid=2650335998&idx=1&sn=33ec033a05a312cdbd8054dc68cc922d&chksm=83aac6c4b4dd4fd2d79898ceea02afa1d593cbf5e8dd3c768a270ad723b9df935770d229b322#rd



在现在人人都有一部手机或电脑的年代,我们几乎天天都在使用各种app,如微信,QQ,抖音,优酷等等软件,表面上我们是与各种app交互,但如果站在操作系统的角度来看,其实我们每天都是在和各种进程或者线程打交道,如果你已经有点疑惑了,没关系,下面我们慢慢来聊个明白。

先看这么几个问题:

1,你手机或者电脑上装的各种app在本质上是什么?

2,一个软件打开和不打开的区别是什么?

3,为什么打开的软件越多就感觉系统越慢?

4,为什么你在用微信语音的时候,还能和别人聊天发消息?

下面我们带着问题来学习一下这其中的知识:

首先对于第一个问题比较简单,我们的各种app其实都是一个软件,描述的再专业一些就是一个程序或者一份能执行的代码。

第二个问题,在使用者层面,打开了就是能用的app,不打开就是一个快捷图标,但在操作系统层面就不一样了,打开时候操作系统实际上会创建一个进程来运行,而关闭的时候,进程就会退出。

那么问题来了,什么是进程:简单的说就是一个正在运行的程序实例。 程序的运行是需要内存和各种操作系统资源的,不同的程序使用的资源是不一样的,比如你打开一个吃鸡游戏和打开一个记事本两者消耗的资源是有很大差异的。

这也就解释了为什么打开的软件越多系统就越卡顿,本质上软件越多,打开的进程就越多,而每个进程都需要一定能资源才能维持运行,我们的操作系统的资源又是有限的,所以占用的越多系统就会越繁忙,就会出现各种卡顿和反应变慢。

仅仅把软件运行起来还是不够的,因为我们还要在软件里面各种交互,比如使用微信语音的同时,还能给对方发图片,或者捞个漂流瓶。再或者使用有道云笔记的时候,你在不停的写字,后台有一个线程会自动每隔一段时间保存一下内存,避免突然断电时内容全部丢失,这底层其实就是使用不同的线程来处理的。

到这里,我们在总结下进程和线程的定义:

进程:一个正在运行的程序实例,包含一个或多个线程,最少有一个线程。

线程:执行进程的一部分程序或者代码指令。

那么他们之间的区别和联系是什么?



(1)进程包含至少一个线程,果只有一个线程,这个线程通常叫做主线程。线程是进程的一部分代码或者指令

(2)进程有独立的内存存储,线程共享进程的内存空间,此外线程有自己独立的的栈存储。

(3)进程通信只能依靠pipe管道或者socket,一个进程内的线程可以直接通信

(4)创建一个进程是重量级操作,而创建一个线程是轻量级操作

(5)进程的文件描述符大部分不共享,线程共享文件描述符

(6)进程有独立的signal信号,线程共享进程的signal信号

(7)进程相互之间不依赖,线程之间有依赖。

(8)进程不需要同步,线程一般需要同步

(9)进程是数据组织的概念,线程是cpu调度的概念

(10) 进程上下文切换慢,线程上下文切换快

最后还有一个问题,子进程与线程有什么关系?这里需要注意子进程也是一个进程,一般用于多任务的操作系统,子进程也称子任务与进程的性质是一样的,也可以包含一个或多个线程。



总结:

本文主要介绍了操作系统中进程和线程的定义,区别和联系。了解这些知识将更加有助于我们学习和使用多线程编程。有一点需要大家注意,进程是程序的运行实例,类似面向对象编程里面的对象,而程序则更像是一个类,通过这个程序我们可以构造多个对象,也就是我们可以启动多个进程,比如PC上的QQ是可以启动数个的,当然跟软件有关系,有些软件只能启动一个,每启动一个程序,其实就是启动了一个进程。如在java的程序里面每启动一个main方法,其实就是启动了一个jvm进程,而main方法就是我们上面所说的进程中至少包含一个线程的主线程。

有什么问题可以扫码关注微信公众号:我是攻城师(woshigcs) 路漫漫其修远兮,吾将上下而求索


]]>
藏在正则表达式里的陷阱 http://doc.okbase.net/chanshuyi/archive/298934.html 陈树义 2018/6/19 9:59:33

文章首发于【博客园-陈树义】,点击跳转到原文《藏在正则表达式里的陷阱》

前几天线上一个项目监控信息突然报告异常,上到机器上后查看相关资源的使用情况,发现 CPU 利用率将近 100%。通过 Java 自带的线程 Dump 工具,我们导出了出问题的堆栈信息。

我们可以看到所有的堆栈都指向了一个名为 validateUrl 的方法,这样的报错信息在堆栈中一共超过 100 处。通过排查代码,我们知道这个方法的主要功能是校验 URL 是否合法。

很奇怪,一个正则表达式怎么会导致 CPU 利用率居高不下。为了弄清楚复现问题,我们将其中的关键代码摘抄出来,做了个简单的单元测试。

public static void main(String[] args) {
    String badRegex = "^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\\\/])+$";
    String bugUrl = "http://www.fapiao.com/dddp-web/pdf/download?request=6e7JGxxxxx4ILd-kExxxxxxxqJ4-CHLmqVnenXC692m74H38sdfdsazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf";
    if (bugUrl.matches(badRegex)) {
        System.out.println("match!!");
    } else {
        System.out.println("no match!!");
    }
}

当我们运行上面这个例子的时候,通过资源监视器可以看到有一个名为 java 的进程 CPU 利用率直接飙升到了 91.4% 。

看到这里,我们基本可以推断,这个正则表达式就是导致 CPU 利用率居高不下的凶手!

于是,我们将排错的重点放在了那个正则表达式上:

^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\/])+$

这个正则表达式看起来没什么问题,可以分为三个部分:

第一部分匹配 http 和 https 协议,第二部分匹配 www. 字符,第三部分匹配许多字符。我看着这个表达式发呆了许久,也没发现没有什么大的问题。

其实这里导致 CPU 使用率高的关键原因就是:Java 正则表达式使用的引擎实现是 NFA 自动机,这种正则表达式引擎在进行字符匹配时会发生回溯(backtracking)。而一旦发生回溯,那其消耗的时间就会变得很长,有可能是几分钟,也有可能是几个小时,时间长短取决于回溯的次数和复杂度。

看到这里,可能大家还不是很清楚什么是回溯,还有点懵。没关系,我们一点点从正则表达式的原理开始讲起。

正则表达式引擎

正则表达式是一个很方便的匹配符号,但要实现这么复杂,功能如此强大的匹配语法,就必须要有一套算法来实现,而实现这套算法的东西就叫做正则表达式引擎。简单地说,实现正则表达式引擎的有两种方式:DFA 自动机(Deterministic Final Automata 确定型有穷自动机)和 NFA 自动机(Non deterministic Finite Automaton 不确定型有穷自动机)。

对于这两种自动机,他们有各自的区别,这里并不打算深入将它们的原理。简单地说,DFA 自动机的时间复杂度是线性的,更加稳定,但是功能有限。而 NFA 的时间复杂度比较不稳定,有时候很好,有时候不怎么好,好不好取决于你写的正则表达式。但是胜在 NFA 的功能更加强大,所以包括 Java 、.NET、Perl、Python、Ruby、PHP 等语言都使用了 NFA 去实现其正则表达式。

那 NFA 自动加到底是怎么进行匹配的呢?我们以下面的字符和表达式来举例说明。

text="Today is a nice day."
regex="day"

要记住一个很重要的点,即:NFA 是以正则表达式为基准去匹配的。也就是说,NFA 自动机会读取正则表达式的一个一个字符,然后拿去和目标字符串匹配,匹配成功就换正则表达式的下一个字符,否则继续和目标字符串的下一个字符比较。或许你们听不太懂,没事,接下来我们以上面的例子一步步解析。

  • 首先,拿到正则表达式的第一个匹配符:d。于是那去和字符串的字符进行比较,字符串的第一个字符是 T,不匹配,换下一个。第二个是 o,也不匹配,再换下一个。第三个是 d,匹配了,那么就读取正则表达式的第二个字符:a。
  • 读取到正则表达式的第二个匹配符:a。那着继续和字符串的第四个字符 a 比较,又匹配了。那么接着读取正则表达式的第三个字符:y。
  • 读取到正则表达式的第三个匹配符:y。那着继续和字符串的第五个字符 y 比较,又匹配了。尝试读取正则表达式的下一个字符,发现没有了,那么匹配结束。

上面这个匹配过程就是 NFA 自动机的匹配过程,但实际上的匹配过程会比这个复杂非常多,但其原理是不变的。

文章首发于【博客园-陈树义】,点击跳转到原文《藏在正则表达式里的陷阱》

NFA 自动机的回溯

了解了 NFA 是如何进行字符串匹配的,接下来我们就可以讲讲这篇文章的重点了:回溯。为了更好地解释回溯,我们同样以下面的例子来讲解。

text="abbc"
regex="ab{1,3}c"

上面的这个例子的目的比较简单,匹配以 a 开头,以 c 结尾,中间有 1-3 个 b 字符的字符串。NFA 对其解析的过程是这样子的:

  • 首先,读取正则表达式第一个匹配符 a 和 字符串第一个字符 a 比较,匹配了。于是读取正则表达式第二个字符。
  • 读取正则表达式第二个匹配符 b{1,3} 和字符串的第二个字符 b 比较,匹配了。但因为 b{1,3} 表示 1-3 个 b 字符串,以及 NFA 自动机的贪婪特性(也就是说要尽可能多地匹配),所以此时并不会再去读取下一个正则表达式的匹配符,而是依旧使用 b{1,3} 和字符串的第三个字符 b 比较,发现还是匹配。于是继续使用 b{1,3} 和字符串的第四个字符 c 比较,发现不匹配了。此时就会发生回溯。
  • 发生回溯是怎么操作呢?发生回溯后,我们已经读取的字符串第四个字符 c 将被吐出去,指针回到第三个字符串的位置。之后,程序读取正则表达式的下一个操作符 c,读取当前指针的下一个字符 c 进行对比,发现匹配。于是读取下一个操作符,但这里已经结束了。

下面我们回过头来看看前面的那个校验 URL 的正则表达式:

^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\/])+$

出现问题的 URL 是:

http://www.fapiao.com/dzfp-web/pdf/download?request=6e7JGm38jfjghVrv4ILd-kEn64HcUX4qL4a4qJ4-CHLmqVnenXC692m74H5oxkjgdsYazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf

我们把这个正则表达式分为三个部分:

  • 第一部分:校验协议。^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)
  • 第二部分:校验域名。(([A-Za-z0-9-~]+).)+
  • 第三部分:校验参数。([A-Za-z0-9-~\\/])+$

我们可以发现正则表达式校验协议 http:// 这部分是没有问题的,但是在校验 www.fapiao.com 的时候,其使用了 xxxx. 这种方式去校验。那么其实匹配过程是这样的:

  • 匹配到 www.
  • 匹配到 fapiao.
  • 匹配到 com/dzfp-web/pdf/download?request=6e7JGm38jf.....,你会发现因为贪婪匹配的原因,所以程序会一直读后面的字符串进行匹配,最后发现没有点号,于是就一个个字符回溯回去了。

这是这个正则表达式存在的第一个问题。

另外一个问题是在正则表达式的第三部分,我们发现出现问题的 URL 是有下划线(_)和百分号(%)的,但是对应第三部分的正则表达式里面却没有。这样就会导致前面匹配了一长串的字符之后,发现不匹配,最后回溯回去。

这是这个正则表达式存在的第二个问题。

解决方案

明白了回溯是导致问题的原因之后,其实就是减少这种回溯,你会发现如果我在第三部分加上下划线和百分号之后,程序就正常了。

public static void main(String[] args) {
    String badRegex = "^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~_%\\\\/])+$";
    String bugUrl = "http://www.fapiao.com/dddp-web/pdf/download?request=6e7JGxxxxx4ILd-kExxxxxxxqJ4-CHLmqVnenXC692m74H38sdfdsazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf";
    if (bugUrl.matches(badRegex)) {
        System.out.println("match!!");
    } else {
        System.out.println("no match!!");
    }
}

运行上面的程序,立刻就会打印出match!!

但这是不够的,如果以后还有其他 URL 包含了乱七八糟的字符呢,我们难不成还再修改一遍。肯定不现实嘛!

其实在正则表达式中有这么三种模式:贪婪模式、懒惰模式、独占模式。

在关于数量的匹配中,有 + ? * {min,max} 四种两次,如果只是单独使用,那么它们就是贪婪模式。

如果在他们之后加多一个 ? 符号,那么原先的贪婪模式就会变成懒惰模式,即尽可能少地匹配。但是懒惰模式还是会发生回溯现象的。TODO例如下面这个例子:

text="abbc"
regex="ab{1,3}?c"

正则表达式的第一个操作符 a 与 字符串第一个字符 a 匹配,匹配成。于是正则表达式的第二个操作符 b{1,3}? 和 字符串第二个字符 b 匹配,匹配成功。因为最小匹配原则,所以拿正则表达式第三个操作符 c 与字符串第三个字符 b 匹配,发现不匹配。于是回溯回去,拿正则表达式第二个操作符 b{1,3}? 和字符串第三个字符 b 匹配,匹配成功。于是再拿正则表达式第三个操作符 c 与字符串第四个字符 c 匹配,匹配成功。于是结束。

如果在他们之后加多一个 + 符号,那么原先的贪婪模式就会变成独占模式,即尽可能多地匹配,但是不回溯。

于是乎,如果要彻底解决问题,就要在保证功能的同时确保不发生回溯。我将上面校验 URL 的正则表达式的第二部分后面加多了个 + 号,即变成这样:

^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)
(([A-Za-z0-9-~]+).)++    --->>> (这里加了个+号)
([A-Za-z0-9-~\\/])+$

这样之后,运行原有的程序就没有问题了。

最后推荐一个网站,这个网站可以检查你写的正则表达式和对应的字符串匹配时会不会有问题。

Online regex tester and debugger: PHP, PCRE, Python, Golang and JavaScript

例如我本文中存在问题的那个 URL 使用该网站检查后会提示:catastrophic backgracking(灾难性回溯)。

当你点击左下角的「regex debugger」时,它会告诉你一共经过多少步检查完毕,并且会将所有步骤都列出来,并标明发生回溯的位置。

本文中的这个正则表达式在进行了 11 万步尝试之后,自动停止了。这说明这个正则表达式确实存在问题,需要改进。

但是当我用我们修改过的正则表达式进行测试,即下面这个正则表达式。

^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+).)++([A-Za-z0-9-~\\\/])+$

工具提示只用了 58 步就完成了检查。

一个字符的差别,性能就差距了好几万倍。

树义有话说

一个小小的正则表达式竟然能够把 CPU 拖垮,也是很神奇了。这也给平时写程序的我们一个警醒,遇到正则表达式的时候要注意贪婪模式和回溯问题,否则我们每写的一个表达式都是一个雷。

通过查阅网上资料,我发现深圳阿里中心 LAZADA 的同学也在 17 年遇到了这个问题。他们同样也是在测试环境没有发现问题,但是一到线上的时候就发生了 CPU 100% 的问题,他们遇到的问题几乎跟我们的一模一样。有兴趣的朋友可以点击阅读原文查看他们后期总结的文章:一个由正则表达式引发的血案 - 明志健致远 - 博客园

虽然把这篇文章写完了,但是关于 NFA 自动机的原理方面,特别是关于懒惰模式、独占模式的解释方面还是没有解释得足够深入。因为 NFA 自动机确实不是那么容易理解,所以在这方面还需要不断学习加强。欢迎有懂行的朋友来学习交流,互相促进。

文章首发于【博客园-陈树义】,点击跳转到原文《藏在正则表达式里的陷阱》

关键词:正则表达式(regex express)、CPU异常(cpu abnormal)、100%CPU、回溯陷阱(backtracking)。

]]>
启航 - cache2go源码分析 http://doc.okbase.net/cloudgeek/archive/298933.html CloudGeek 2018/6/19 9:59:27

一、概述

  我们今天开始第一部分“golang技能提升”。这一块我计划分析3个项目,一个是很流行的golang源码阅读入门项目cache2go,接着是非常流行的memcache的go语言版groupcache,最后是docker项目中分出来的核心组件之一containerd,通过这3个项目的源码全解析来达到golang能力上一个台阶的目的。

  在本系列教程的学习过程中,我希望你能够做到如下要求:如果你是一个go语言新手,在看下面的代码分析过程中你肯定会遇到一些自己陌生的编码方式、陌生的知识点,这个过程中我希望你遇到一个点掌握一个点,比如看到下面的代码用到了锁,就去找各种资料把锁相关的知识点学了。看到回调函数,就思考一下人家为什么这些地方都使用回调函数,有什么好处。这样在看完这个项目源码分析后,你就能学到一部分知识模块

二、cache2go是什么

  这是一个在github上开源的项目,原作者这样介绍:

  Concurrency-safe golang caching library with expiration capabilities.

  看懂了吗?简单说就是有心跳机制的并发安全的go语言缓存库。ok,下面我们要分析的这个项目是一个缓存库,并且有2大特性,并发安全和心跳机制!

 

三、项目结构

项目目录结构如上图所示,可以看到功能实现相关源码文件只有3个:

  • cache.go

  • cacheitem.go

  • cachetable.go

四、关键数据结构

   项目中只涉及到2个复杂的数据类型,分别是:

  • CacheItem

  • CacheTable

   含义和字面意义一致,一个是缓存表,一个是缓存表中的条目。下面分别看一下这2个结构是怎么定义的。

 

1、CacheItem

  CacheItem类型是用来表示一个单独的缓存条目,源码如下所示,每个字段都很清晰易懂,注释稍长的属性已经中文标注。

 1// CacheItem is an individual cache item
2// Parameter data contains the user-set value in the cache.
3type CacheItem struct {
4    sync.RWMutex
5
6    // The item's key.
7    key interface{}
8    // The item's data.
9    data interface{}
10    //【不被访问后存活时间】
11    // How long will the item live in the cache when not being accessed/kept alive.
12    lifeSpan time.Duration
13
14    // Creation timestamp.
15    createdOn time.Time
16    // Last access timestamp.
17    accessedOn time.Time
18    // How often the item was accessed.
19    accessCount int64
20    //【被删除时触发的回调方法】
21    // Callback method triggered right before removing the item from the cache
22    aboutToExpire func(key interface{})
23}

2、CacheTable

  CacheTable描述了缓存中的一个表项,里面的items属性就是上面讲到的CacheItem类型实例。这里除了常规属性外还有若干函数类型的属性,源码如下所示(最后几行显示样式好像抽筋了~):

 1// CacheTable is a table within the cache
2type CacheTable struct {
3    sync.RWMutex
4
5    // The table's name.
6    name string
7    // All cached items.
8    //【一个表中的所有条目都存在这个map里,map的key是任意类型,值是CacheItem指针类型】
9    items map[interface{}]*CacheItem
10
11    // Timer responsible for triggering cleanup.
12    //【负责触发清除操作的计时器】
13    cleanupTimer *time.Timer
14    // Current timer duration.
15    //【清除操作触发的时间间隔】
16    cleanupInterval time.Duration
17
18    // The logger used for this table.
19    logger *log.Logger
20
21    // Callback method triggered when trying to load a non-existing key.
22    //【需要提取一个不存在的key时触发的回调函数】
23    loadData func(key interface{}, args ...interface{}) *CacheItem
24    // Callback method triggered when adding a new item to the cache.
25    //【增加一个缓存条目时触发的回调函数】
26    addedItem func(item *CacheItem)
27    // Callback method triggered before deleting an item from the cache.
28    //【删除前触发的回调函数】
29    aboutToDeleteItem func(item *CacheItem)
30}

  如上所示,cache2go的核心数据结构很简洁,应该比较容易理解。当然没有完全理解每个字段这样定义的原因也别急,看完下面的代码逻辑后反过来再看数据结构,肯定就明白每个字段的作用了。

五、代码逻辑

   我们的思路是从下往上分析代码,什么意思呢?就是说先看和item相关的操作,再看包含item的table相关的代码,最后看操作table的cache级别的逻辑。所以下面我们要先看cacheitem.go的代码,接着分析cachetable.go,然后看cache.go,最后看3个文件中的源码互相间关联是什么,最后看example,也就是cache怎么玩~

1、cacheitem.go

  如上图,这个源码文件中只包含了一个类型CacheItem和一个函数NewCacheItem()的定义。CacheItem有哪些属性前面已经看过了,下面先看NewCacheItem()函数:

 1// NewCacheItem returns a newly created CacheItem.
2// Parameter key is the item's cache-key.
3// Parameter lifeSpan determines after which time period without an access the item
4// will get removed from the cache.
5// Parameter data is the item's value.
6func NewCacheItem(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
7    t := time.Now()
8    return &CacheItem{
9        key:           key,
10        lifeSpan:      lifeSpan,
11        createdOn:     t,
12        accessedOn:    t,
13        accessCount:   0,
14        aboutToExpire: nil,
15        data:          data,
16    }
17}
  •  代码如上所示,NewCacheItem()函数接收3个参数,分别是键、值、存活时间(key、data、lifeSpan),返回一个CacheItem类型实例的指针。

  • 其中createOn和accessedOn设置成了当前时间,aboutToExpire也就是被删除时触发的回调方法暂时设置成nil,不难想到这个函数完成后还需要调用其他方法来设置这个属性。

  cacheitem.go中除了CacheItem类型的定义,NewCacheItem()函数的定义外,还有一个部分就是CacheItem的方法定义,一共8个

  源码看起来行数不少,内容其实很简单,主要是元素获取操作,这里需要留意读写操作都是加了相应的读写锁的,还记得开头提到的cache2go是一个并发安全的程序吗?并发安全就体现在这些地方。下面最复杂的是最后一个回调函数的设置,这个方法的形参是f func(interface{}),也就是说形参名为f,形参类型是func(interface{}),这是一个函数类型,这个函数类型的参数是一个interface{},也就是空接口,因为任意类型都可以被认为实现了空接口,所以这里可以接收任意类型的实参。也就是说f的类型是一个可以接收任意类型参数的函数类型。有点绕,需要静下心来理解一下哦~

  源码如下:

 1// KeepAlive marks an item to be kept for another expireDuration period.
2//【将accessedOn更新为当前时间】
3func (item *CacheItem) KeepAlive() {
4    item.Lock()
5    defer item.Unlock()
6    item.accessedOn = time.Now()
7    item.accessCount++
8}
9
10// LifeSpan returns this item's expiration duration.
11func (item *CacheItem) LifeSpan() time.Duration {
12    // immutable
13    return item.lifeSpan
14}
15
16// AccessedOn returns when this item was last accessed.
17func (item *CacheItem) AccessedOn() time.Time {
18    item.RLock()
19    defer item.RUnlock()
20    return item.accessedOn
21}
22
23// CreatedOn returns when this item was added to the cache.
24func (item *CacheItem) CreatedOn() time.Time {
25    // immutable
26    return item.createdOn
27}
28
29// AccessCount returns how often this item has been accessed.
30func (item *CacheItem) AccessCount() int64 {
31    item.RLock()
32    defer item.RUnlock()
33    return item.accessCount
34}
35
36// Key returns the key of this cached item.
37func (item *CacheItem) Key() interface{} {
38    // immutable
39    return item.key
40}
41
42// Data returns the value of this cached item.
43func (item *CacheItem) Data() interface{} {
44    // immutable
45    return item.data
46}
47
48// SetAboutToExpireCallback configures a callback, which will be called right
49// before the item is about to be removed from the cache.
50//【设置回调函数,当一个item被移除的时候这个函数会被调用】
51func (item *CacheItem) SetAboutToExpireCallback(f func(interface{})) {
52    item.Lock()
53    defer item.Unlock()
54    item.aboutToExpire = f
55}

  到这里就看完这个源文件了,是不是很轻松呢~

  上面的一堆方法功能都还是很直观的,今天我们先看到这里,下一讲继续分析cachetable相关代码。

 

 

]]>
python logging详解及自动添加上下文信息 http://doc.okbase.net/xybaby/archive/298932.html xybaby 2018/6/19 9:36:26

 

  之前写过一篇文章日志的艺术(The art of logging),提到了输出日志的时候记录上下文信息的重要性,我认为上下文信息包括:

  • when:log事件发生的时间
  • where:log事件发生在哪个模块(文件、函数)
  • how important:log 事件的紧急程度
  • who:事件产生者的唯一标识
  • what:具体的事件内容,以及其他所必须的上下文信息

  其中,when、where、how important都很容易通过logging框架自动包含,但是who(事件生产者的唯一标识)就不能框架自动填充了。比如在服务器端,同时有大量的用户,如果日志缺乏用户唯一标识,如(User can not login),那么可以说这样的日志是毫无意义的。特别是当线上出问题的时候,而且是偶发的问题的时候,日志往往是查找问题的唯一途径,如果这个时候日志信息不充分,那就很让人抓狂了。

  虽然在团队中强调过很多次,但是很多同事还是会输出毫无意义的log,也曾试过静态代码检查或者运行时分析,不过都不太优雅,于是希望能够自动添加一些重要的上下文信息,比如用户唯一标识。

  虽然每天都在打log,但事实上我以前也没有深度了解过python logging模块,于是借着这个机会,仔细看了一下logging文档与源码。

  这里补充一句,虽然网上有很多文章介绍python logging模块,但还是建议直接看官方资料,顺序如下:tutorialapi documentcookbook源码

  本文地址:https://www.cnblogs.com/xybaby/p/9197032.html

初识python logging

  logging模块是python官方提供的、标准的日志模块。看代码的话是借鉴了许多log4j中的概念和思想。

  logging模块包括以下几大组件:

  • Loggers expose the interface that application code directly uses.
  • Handlers send the log records (created by loggers) to the appropriate destination.
  • Filters provide a finer grained facility for determining which log records to output.
  • Formatters specify the layout of log records in the final output.

  下面结合一个更完整的例子来逐个介绍。example1.py

 1 import logging
 2 class ContextFilter(logging.Filter):
 3     def filter(self, record):
 4         record.userid = '123'
 5         return True
 6 
 7 
 8 if __name__ == '__main__':
 9     # create logger
10     logger = logging.getLogger('simple_example')
11     logger.setLevel(logging.DEBUG)
12 
13     # create console handler and set level to debug
14     ch = logging.StreamHandler()
15     ch.setLevel(logging.DEBUG)
16     # create formatter for console handler
17     formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18     # add formatter to console handler
19     ch.setFormatter(formatter)
20 
21     # create file handler and set level to warn
22     fh = logging.FileHandler('spam.log')
23     fh.setLevel(logging.WARN)
24     # create formatter for file handler
25     formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(userid)s - %(message)s')
26     # add formatter to file handler
27     fh.setFormatter(formatter)
28     # add context filter to file handler
29     fh.addFilter(ContextFilter())
30 
31     # add ch、fh to logger
32     logger.addHandler(ch)
33     logger.addHandler(fh)
34 
35     # 'application' code
36     logger.debug('debug message')
37     logger.info('info message')
38     logger.warn('warn message %s', 'args')
39     logger.error('error message')
40     logger.critical('critical message')

  console输出结果:

2018-06-10 10:44:32,342 - simple_example - DEBUG - debug message
2018-06-10 10:44:32,342 - simple_example - INFO - info message
2018-06-10 10:44:32,342 - simple_example - WARNING - warn message args
2018-06-10 10:44:32,342 - simple_example - ERROR - error message
2018-06-10 10:44:32,342 - simple_example - CRITICAL - critical message

  文件内容:

2018-06-10 10:44:32,342 - simple_example - WARNING - 123 - warn message args
2018-06-10 10:44:32,342 - simple_example - ERROR - 123 - error message
2018-06-10 10:44:32,342 - simple_example - CRITICAL - 123 - critical message

logging模块解析

logger

  logger是直接提供给应用程序使用的接口。一般来说,logging的其他几个组件(Handler、Filter、Formatter)都是在初始化Logger的时候使用。

  logger提供了以下API(部分、重要的):

Logger.setLevel(level)
  Sets the threshold for this logger to level. Logging messages which are less severe than level will be ignored.
Logger.isEnabledFor(lvl)
  Indicates if a message of severity lvl would be processed by this logger. 
Logger.debug(msg, *args, **kwargs)
  Logs a message with level DEBUG on this logger.

Logger.info(msg, *args, **kwargs)
  Logs a message with level INFO on this logger.

Logger.warning(msg, *args, **kwargs)
  Logs a message with level WARNING on this logger. same as Logger.warn

Logger.error(msg, *args, **kwargs)
  Logs a message with level ERROR on this logger.

Logger.critical(msg, *args, **kwargs)
  Logs a message with level CRITICAL on this logger. same as Logger.fatal

Logger.addFilter(filter)
  Adds the specified filter filter to this logger.
Logger.removeFilter(filter)
  Removes the specified filter filter from this logger.
Logger.addHandler(hdlr)
  Adds the specified handler hdlr to this logger.
Logger.removeHandler(hdlr)
  Removes the specified handler hdlr from this logger. 

logger实例

  Logger对象不是通过实例化Logger而来的,都是通过 logging.get_logger(name)  获得一个与name关联的logger对象,logging内部会维护这个映射关系,用同样的name反复调用logging.getLogger,事实上返回的是同一个对象。

Multiple calls to getLogger() with the same name will always return a reference to the same Logger object.

  logger有一个父子关系,比如:

1 a_logger = logging.getLogger('A’) 
2 ab_logger = logging.getLogger('A.B’)

   通过name的层次关系就可以看出a_logger是ab_logger的parent。这个父子关系是会影响到日志的输出的,后面介绍logging流程的时候可以看到。不过我并没有使用过这种层级关系,所以本文也不会展开介绍。

log level

  log level即日志的重要等级,意味着不同的紧急程度,不同的处理方式。python logging中,包含以下log level:

  

  上表也给出了什么时候使用什么级别的Log level的建议。

  只有当写日志时候的log level大于等于logger的level阈值(通过setLevel设置)时,日志才可能被输出。

  比如example1.py中,输出到文件时,debug message、info message并没有输出,因为file handler的logger level是warning,DEBUG和INFO都小于WARNING。

FIlter && Handler

  从logger中Filter、Handler相关的接口可以看到一个logger可以包含多个handler,多个Filter

  在example1.py中,logger对象指定了两个handler,第14行的StreamHandler,以及第16行的FileHandler。

Handler

  Handler定义了如何处理一条日志,是本地打印,还是输出到文件,还是通过网络发送。

  可以这么理解,日志代表的是一个事件(event),输出日志的应用程序是生产者,而handler是消费者,消费者可以有多个。

  Handler有以下API:

Handler.setLevel(level)
  Sets the threshold for this handler to level. Logging messages which are less severe than level will be ignored. 
Handler.setFormatter(fmt)
  Sets the Formatter for this handler to fmt.
Handler.addFilter(filter)
  Adds the specified filter filter to this handler.
Handler.removeFilter(filter)
  Removes the specified filter filter from this handler.

  对于上述API,首先setFormatter表明了日志的格式,这里是“set”,而不是“add”,这个很好理解。其次,Handler有一些与Logger相同的函数:setLevel、addFilter、removeFilter。为啥要用相同的API,在tutorial中是这样介绍的:

Why are there two setLevel() methods? The level set in the logger determines which severity of messages it will pass to its handlers. The level set in each handler determines which messages that handler will send on.

  但个人认为这个解释很牵强,我觉得在handler(日志事件的消费者)指定过滤条件更合理、更直观。在生产者添加logLevel的目的只是为了开发调试方便:开发的时候通过debug输出调试信息,开发的时候设置高于DEBUG的log level来避免输出这些调试信息。

  python logging提供了大量的实用的Handler,可以写到标准输出、文件、linux syslog、邮件、远程服务器。

Formatter

responsible for converting a LogRecord to (usually) a string which can be interpreted by either a human or an external system  

  可以看到,Formatter将LogRecord输出成字符串。应用程序每输出一条日志的时候,就会创建一个LogRecord对象。看看上述例子:

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger.warn('warn message %s', 'args')

对应输出:
  2018-06-10 10:44:32,342 - simple_example - WARNING - warn message args

  可以看到,Formatter指定了输出了格式与内容,在logger.warn的两个输入参数组成了formatter中的 %(message)s ,而formatter中的 %(asctime) 等则是在生成LogRecord填充

  以下是默认提供的属性:

  

  如果一个handler没有指定Formatter,那么默认的formatter就是  logging.Formatter('%(message)s')  ,在上面的例子中,对于warn这条日志,那么输出结果是 warn message args 

Filter

Filters can be used by Handlers and Loggers for more sophisticated filtering than is provided by levels.

filter(record)

  Is the specified record to be logged? Returns zero for no, nonzero for yes. If deemed appropriate, the record may be modified in-place by this method.

  Filter提供的是比log level更精确的过滤控制,只有当Filter.filter函数返回True的时候,这条日志才会被输出。

  由于Filter.filter函数的参数是LogRecord实例,那么可以修改LogRecord的属性。在example1.py 第25行    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(userid)s - %(message)s')  file formatter中指定了属性%(userid)s, 但userid并不在Formatter的属性列表中,这个userid其实就是通过自定义的ContextFilter来实现的,在ContextFilter.filter中,给record对象添加了userid属性。后面还会细讲。

  官方给出了很多例子,都是通过Filter来修改LogRecord的内容,来输出一些上下文信息(contextual information)。但是我觉得这是一个不好的设计,顾名思义,Filter应该只做过滤的事情,理论上不应该修改输入,builtin filter函数就是这样的。

logging流程

  从上述的介绍中可以看出,应用层打印的一条日志,需要经过很多步才能最终落地;既受到logger(生产者)的log level、Filter过滤,又要受到Handler(消费者)的log level、Filter过滤。

  tutorial中给出了一个完整的流程图:

  

 

logging性能

lazy logging

  在日志的艺术(The art of logging)一文中提到了lazy logging,即避免过早求值,把字符串的格式化推迟。但对于下面的语句

logger.debug('Message with %s, %s', expensive_func1(), expensive_func2())

  虽然不会过早的字符串格式化(format),但是expensive_func1、expensive_func2这两个函数一定会调用,不管日志是否会被输出。如何避免调用这两个函数,解决办法是这样的

if logger.isEnabledFor(logging.DEBUG):
    logger.debug('Message with %s, %s', expensive_func1(),
                                        expensive_func2())

  即先判断是否能够输出DEBUG等级的log,然后再调用logger.debug,这样就避免了调用expensive_func1、expensive_func2这两个函数

  在这里,除了优化性能,还有一个好处就是让代码的意图更加明显(explicit),如果expensive_func是有副作用(side affect),即会改变某些内部状态,那么用这样的代码能更清楚表达expensive_func 不一定会被调用。当然,写在日志里面的表达式不应该有任何副作用、函数也应该是pure function。

性能开关

  python logging模块的性能是较差的,不仅仅是Python语言的问题,更多的是logging模块的实现问题。

  前面提到,应用程序的每一条日志都会生成一个LogRecord对象,然后Formatter将其转换成string,怎么转化的呢,其实核心就一句代码  s = self._fmt % record.__dict__  self是Formatter实例,_fmt即初始化Formatter时传入的参数,record即LogRecord实例。由于LogRecord不知道Formatter需要用到哪些属性,所以干脆计算出所有的属性(见图一),这是eager evaluation,lazy evaluation应该更适合

  也许Formatter只用到了%(message)s 属性,即只需要对logger.debug的输入参数求值即可,但是LogRecord还是会去反复计算线程id、进程id、应用程序的调用信息(文件、函数、行号),这些都是很耗的操作。

  对于部分确定不会用到的属性,可以通过开关关掉:

What you don’t want to collectHow to avoid collecting it
Information about where calls were made from. Set logging._srcfile to None. This avoids calling sys._getframe(), which may help to speed up your code in environments like PyPy (which can’t speed up code that uses sys._getframe()).
Threading information. Set logging.logThreads to 0.
Process information. Set logging.logProcesses to 0.

  

  通过下面的代码测试(注释第3到6行),关掉之后有25%左右的性能提升

 1 import time
 2 import logging
 3 logging._srcfile = None
 4 logging.logThreads = 0
 5 logging.logMultiprocessing = 0
 6 logging.logProcesses = 0
 7 
 8 if __name__ == '__main__':
 9     logger = logging.getLogger('simple_example')
10     logger.setLevel(logging.DEBUG)
11 
12     null = logging.NullHandler()
13     logger.addHandler(null)
14 
15     begin = time.time()
16     for i in xrange(100000):
17         logger.debug('debug message')
18         logger.info('info message')
19         logger.warn('warn message')
20         logger.error('error message')
21         logger.critical('critical message')
22     print 'cost', time.time() - begin

 

  进一步,logging为了线程安全,每个Handler都会持有一个互斥锁,每次写日志的时候都会获取锁,写完之后再释放锁。由于GIL的问题,多线程并不能给某些类型的python程序代码性能提升。因此,如果应用程序能保证是单线程,那么可以设置  logging.thread = None  来避免使用锁,简单测试过也会有15%左右性能提升.

logging实用

  再来看几个logging使用方法或者技巧

同一个logger,不同的handler

  在前面介绍logging.Logger的时候已经提到,一个logger(生产者)可以有多个handler(消费者)。不同的handler可以有不同的输出格式(Formatter),不同的输出过滤条件(level、Filter)。

  比如对于线上项目,会有INFO、WARN、ERROR(FATAL)级别的日志产生。不同级别的日志待遇是不一样的,INFO级别的日志,一般只会在查问题的时候用到,记录到文件就行了;而WARN及以上级别的日志,就需要被监控系统采集、统计,会使用到SysLogHandler,然后配合ELK;而对于ERROR或者FATAL级别的日志,需要直接给值班账号发邮件(短信)。

  对于SyslogHandler,使用syslog_tag来设置分发方式是比较高效的,具体可见由一个简单需求到Linux环境下的syslog、unix domain socket。需要注意的是,默认情况下每一个handler都会持有一个unix domain socket的文件描述符。

上下文信息(contextual information)

  回到文章开始的问题,如何在日志中输出必须的上下文信息,还是以用户唯一标识(userid)为例。

  要做到这个事情,有不同的策略。上策是自动化,自动输出补全上下文信息;中策是检测,如果不包含上下文信息,静态代码检查或者运行时报错;下策是强调,这个最不靠谱

  在前面已经提到,将LogRecord输出成字符串核心就是   s = self._fmt % record.__dict__   ,_fmt是Formatter初始化格式,如果我们需要在_fmt中添加了自定义属性,如userid,那么只需要保证record.__dict__中有 ‘userid’ 这个key就行了。

  接下来看看具体的方法:

第一:使用Filter

  如example2所示,直接在给record设置了userid属性。

  但使用Filter有一个缺点,不太灵活,Filter一般在初始化Logger(handler)的时候使用,但userid信息实在调用时才能明确的,而且显然不会只有一个useid。

第二:使用extra参数

  再来看看  Logger.debug(msg, *args, **kwargs) 

  kwargs中可以包含两个特殊的key:exc_info, extra。

The second keyword argument is extra which can be used to pass a dictionary which is used to populate the __dict__ of the LogRecord created for the logging event with user-defined attributes. These custom attributes can then be used as you like. For example, they could be incorporated into logged messages. 

  具体怎么做到的?看源码就很清楚了

 1 def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
 2         """
 3         A factory method which can be overridden in subclasses to create
 4         specialized LogRecords.
 5         """
 6         rv = LogRecord(name, level, fn, lno, msg, args, exc_info, func)
 7         if extra is not None:
 8             for key in extra:
 9                 if (key in ["message", "asctime"]) or (key in rv.__dict__):
10                     raise KeyError("Attempt to overwrite %r in LogRecord" % key)
11                 rv.__dict__[key] = extra[key]
12         return rv

 

  在生成LogRecord的时候会直接把extra的值赋给LogRecord.__dict__.

  因此,example2代码可以改写成这样:

 1 import logging
 2 
 3 if __name__ == '__main__':
 4     # create logger
 5     logger = logging.getLogger('simple_example')
 6     logger.setLevel(logging.DEBUG)
 7 
 8     # create console handler and set level to debug
 9     ch = logging.StreamHandler()
10     ch.setLevel(logging.DEBUG)
11     # create formatter for console handler
12     formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
13     # add formatter to console handler
14     ch.setFormatter(formatter)
15 
16     # create file handler and set level to warn
17     fh = logging.FileHandler('spam.log')
18     fh.setLevel(logging.WARN)
19     # create formatter for file handler
20     formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(userid)s - %(message)s')
21     # add formatter to file handler
22     fh.setFormatter(formatter)
23 
24     # add ch、fh to logger
25     logger.addHandler(ch)
26     logger.addHandler(fh)
27 
28     # 'application' code
29     logger.debug('debug message', extra={'userid': '1'})
30     logger.info('info message', extra={'userid': '2'})
31     logger.warn('warn message %s', 'args', extra={'userid': '3'})
32     logger.error('error message', extra={'userid': '4'})
33     logger.critical('critical message', extra={'userid': '5'})

 

  使用extra,比Filter更加灵活,每一条日志都可以根据实际情况指定不同的userid。但问题是每一条日志都需要写额外的extra参数,既是在某些环节下extra参数内容都是一样的。

  当然,如果漏了extra,会报trace,也就是上面提到的中策的效果。

第三:使用LoggerAdapter

   使用LoggerAdapter避免了每条日志需要添加extra参数的问题,达到了上策的效果。LoggerAdapter实现很简单,一看代码就明白。

  

 1 class LoggerAdapter(object):
 2     def __init__(self, logger, extra):
 3         self.logger = logger
 4         self.extra = extra
 5 
 6     def process(self, msg, kwargs):
 7         """
 8         Process the logging message and keyword arguments passed in to
 9         a logging call to insert contextual information. You can either
10         manipulate the message itself, the keyword args or both. Return
11         the message and kwargs modified (or not) to suit your needs.
12         Normally, you'll only need to override this one method in a
13         LoggerAdapter subclass for your specific needs.
14         """
15         kwargs["extra"] = self.extra
16         return msg, kwargs
17 
18     def debug(self, msg, *args, **kwargs):
19         msg, kwargs = self.process(msg, kwargs)
20         self.logger.debug(msg, *args, **kwargs)

 

  直接使用LoggerAdapter.extra覆盖掉kwargs['extra‘],当然我觉得覆盖并不是一个好主意,用update应该更合适。

  假设类UserRequest处理用户请求,那么每个UserRequest实例都持有一个   LoggerAdapter(logger, {'userid': realuserid})  ,这样写日志的时候就不会额外指定上下文信息(userid)了

 缓存日志内容,延迟输出

  来自cookbook buffering-logging-messages-and-outputting-them-conditionally

  这是个非常有趣,且有用的想法。某些场景下,我们需要缓存一些日志,只有当满足某些条件的时候再输出。比如在线上查bug,需要加一些额外的调试(DEBUG或INFO)日志,但是只有当bug复现的情况下(ERROR日志标志问题复现了)才需要打印这些调试日志,因此可以先缓存,当出现了ERROR日志的时候再打印出所有的调试日志。

@log_if_errors(logger)
def foo(fail=False):

 

  不过上面的例子只适合单个函数调用的情况,而实际环境中,查一个BUG可能需要追踪的是一个调用流程。

总结

  python logging模块还有一些功能并未在文章介绍,比如丰富多样的Handler,比如各种logger配置方式(除了在代码中指定loglevel、formatter、filter,还可以写配置文件,甚至从网络读取配置)。

  python logging本身功能就有丰富,而且也比较好扩展,如果有什么需求,建议先好好看看文档。

references

日志的艺术(The art of logging)

logging — Logging facility for Python

python logging tutorial

logging cookbook

 

]]>