小练习:RSS解析

题目描述

RSS(Really Simple Syndication)是一种描述和同步网站内容的格式,基于XML,能够被客户端解析程序用做数据源。

1、使用tinyXml2解析RSS文件,并生成一个网页库pagelib.dat

tinyXml2 -- https://github.com/leethomason/tinyxml2
rss      -- https://coolshell.cn/feed
         -- http://www.runoob.com/rss/rss-tutorial.html

参考接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct RssItem
{
string title;
string link;
string description;
string content;
};

class RssReader
{
public:
RssReader();
void parseRss();//解析
void dump(const string & filename);//输出
private:
vector<RssItem> _rss;
};

要求:最后生成一个 pagelib.txt, 其格式:

<doc>
    <docid>1</docid>
    <title>...</title>
    <link>...</link>
    <description>...</description>
    <content>...</content>
</doc>
<doc>
    <docid>2</docid>
    <title>...</title>
    <link>...</link>
    <description>...</description>
    <content>...</content>
</doc>
<doc>
  ...
</doc>

RSS文件解析作业思路:
xml –>rss–>tinyxml2(使用该库对rss文件进行解析)–> std::regex(使用正则表达式去除html标签)

提示:首先去读coolshell.xml文件,因为是一个rss文件,而我们需要找到rss的channel节点下面的item节点的title节点、link节点中间的文本,至于这些文本可以使用tinyxml2这个第三方库进行解析,所以这里需要看看timyxml2如何解析第三方库(可以看看timyxml2的源码),解析完成一个item后,可以将其存到vector中(也可以将这些原始信息经过后面正则表达式去除标签后再存起来),然后再去读第二个item(其实就是一个循环操作),其实第二个item是第一个item的兄弟节点(可以使用tinyxml2里面的函数进行跳转到第二个item),这样就可以解析coolshell.xml文档了。接着,因为description信息中包含html的标签,所以需要去除这样的html标签,如<p>``</p>,这个可以使用正则表达式,也就是std::regex进行去除,这个可以在cppreference.html中进行查找使用方法。

最后就是遍历vector,讲读取到的信息存到另外一个文件,格式需要自己定义,使用我们自定义的<doc> </doc>格式

rss文件其实就是一个树形结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<rss>
<channel>
<item>
<title> </title>
<link> </link>
<description> </description>
<content> </content>
</item>

<item>
<title> </title>
<link> </link>
<description> </description>
<content> </content>
</item>
<item>
</item>
</channel>
</rss>

第三方库的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int example_3()
{
static const char* xml =
"<?xml version=\"1.0\"?>"
"<!DOCTYPE PLAY SYSTEM \"play.dtd\">"
"<PLAY>"
"<TITLE>A Midsummer Night's Dream</TITLE>"
"</PLAY>";

XMLDocument doc;
doc.Parse( xml );

XMLElement* titleElement = doc.FirstChildElement( "PLAY" )->FirstChildElement( "TITLE" );
const char* title = titleElement->GetText();
printf( "Name of play (1): %s\n", title );

XMLText* textNode = titleElement->FirstChild()->ToText();
title = textNode->Value();
printf( "Name of play (2): %s\n", title );

return doc.ErrorID();
}

但是这里会有几个小问题

1.xml文件中的channel标签其实是二级标签,所以按实例里写的

1
XMLElement* titleElement = doc.FirstChildElement( "PLAY" )->FirstChildElement( "TITLE" );

这样写会发生段错误,所以应该这样写

1
XMLElement* titleElement = doc.FirstChildElement()->FirstChildElement( "channel" )->FirstChildElemen("title");

才能准确获取到channel标签下的title这个XMLElement节点,然后按照实例里面的取出标签里的内容就可以了

2.channel标签下的title标签可能有很多个,所以该怎么获取下一个title标签呢?

可以使用XMLElement的NextSiblingElement()来获取下一个兄弟节点

完整写法为

1
2
XMLElement* titleElement = doc.FirstChildElement()->FirstChildElement( "channel" )->FirstChildElemen("title");
titleElement = titleElement->NextSiblingElement("title");//此时的titleElement就是下一个title节点了,然后按照实例里面的方法取出来该title包裹的文本内容即可

代码实现

头文件 xml.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#pragma once
#include "tinyxml2.h"
#include <string>
#include <vector>
#include <regex>
#include <fstream>
#include <iostream>
using namespace std;
using namespace tinyxml2;
struct RssItem
{
string title;
string link;
string description;
string content;
};


class RssReader{
public:
RssReader();
void parseRss();
void dump(const string & filename);
void output(const string & filename="pagelib.txt");
~RssReader();
private:
string removerHtmlTags(const string &html);
private:
vector<RssItem> _rss;
char * _xml;
};

实现文件xml.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include "3_xml.hpp"


RssReader::RssReader() = default;

void RssReader::dump(const string &filename){
fstream fd(filename,ios::in |ios::ate);
if(!fd.is_open()){
return;
}
int length = fd.tellg();
fd.seekg(0,ios::beg);
_xml = new char[length +1]();
fd.read(_xml,length);//将xml文件读入

fd.close();
}



string RssReader::removerHtmlTags(const string &html){
regex htmlTags("<[^>]*>");
return regex_replace(html,htmlTags,"");
}


void RssReader::parseRss(){
XMLDocument doc;
doc.Parse(_xml);
cout << "parse xml" << endl;
XMLElement *item = doc.FirstChildElement()->FirstChildElement("channel")->FirstChildElement("item");
cout << "get item" << endl;
//只要一直能读到就一直读
while(item){
cout << "before title" << endl;
XMLElement *titleElement = item->FirstChildElement("title");
string title = "";
if(titleElement){
title = removerHtmlTags(titleElement->GetText());
}
cout << "after title" << endl;
cout << "before link" << endl;
XMLElement *linkElement = item->FirstChildElement("link");
string link = "";
if(linkElement){
link = removerHtmlTags(linkElement->GetText());
}
cout << "after link" << endl;
cout << "before description" <<endl;
XMLElement *descriptionElement = item->FirstChildElement("description");
string description = "";
if(descriptionElement){
description = removerHtmlTags(descriptionElement->GetText());
}
cout << "after description" <<endl;
cout << "before content" << endl;
XMLElement *contentElement = item->FirstChildElement("content:encoded");
string content = "";
if(contentElement){
content = removerHtmlTags(contentElement->GetText());
}
cout <<"after content" <<endl;
RssItem ite;
ite.title = title;
ite.link = link;
ite.description = description;
ite.content = content;

_rss.push_back(ite);

item = item->NextSiblingElement("item");
}
}


void RssReader::output(const string & filename){
ofstream outFile(filename.c_str());
int num = 1;
for(auto &rss_item :_rss){
outFile << "<doc>" << endl;
outFile << "\t<docid>" << num <<"</docid>"<<endl;
outFile <<"\t<title>" << rss_item.title << "</title>"<<endl;
outFile <<"\t<link>" << rss_item.link << "</link>" << endl;
outFile << "\t<description>" << rss_item.description << "</description>"<<endl;
outFile << "\t<content>" << rss_item.content << "</content>" <<endl;
outFile << "</doc>" <<endl;
++num;
}
}

RssReader::~RssReader(){
if(_xml){
delete [] _xml;
_xml = nullptr;
}
}

void test(){
RssReader ri;
ri.dump("coolshell.xml");
ri.parseRss();
ri.output();
}



int main()
{
test();
return 0;
}

程序运行的部分结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<doc>
<docid>1</docid>
<title>ETCD的内存问题</title>
<link>https://coolshell.cn/articles/22242.html</link>
<description>今天跟大家分享一个etcd的内存大量占用的问题,这是前段时间在我们开源软件Easegress中遇到的问题,问题是比较简单的,但是我还想把前因后果说一下,包括,为...
Read More Read More
The post ETCD的内存问题 first appeared on 酷 壳 - CoolShell.</description>
<content>今天跟大家分享一个etcd的内存大量占用的问题,这是前段时间在我们开源软件Easegress中遇到的问题,问题是比较简单的,但是我还想把前因后果说一下,包括,为什么要用etcd,使用etcd的用户场景,包括etcd的一些导致内存占用比较大的设计,以及最后一些建议。希望这篇文章不仅仅只是让你看到了一个简单的内存问题,还能让你有更多的收获。当然,也欢迎您关注我们的开源软件,给我们一些鼓励。
为什么要用ETCD
先说一下为什么要用etcd。先从一个我们自己做的一个API网关 &#8211; Easegress(源码)说起。
Easegress 是我们开发并开源的一个API应用网关产品,这个API应用网关不仅仅只是像nginx那样用来做一个反向代理,这个网关可以做的事很多,比如:API编排、服务发现、弹力设计(熔断、限流、重试等)、认证鉴权(JWT,OAuth2,HMAC等)、同样支持各种Cloud Native的架构如:微服务架构,Service Mesh,Serverless/FaaS的集成,并可以用于扛高并发、灰度发布、全链路压力测试、物联网……等更为高级的企业级的解决方案。所以,为了达到这些目标,在2017年的时候,我们觉得在现有的网关如Nginx上是无法演进出来这样的软件的,必需重新写一个(后来其他人也应该跟我们的想法一样,所以,Lyft写了一个Envoy。只不过,Envoy是用C++写的,而我用了技术门槛更低的Go语言)
另外,Easegress最核心的设计主要有三个:


一是无第三方依赖的自己选主组集群的能力
二是像Linux管道命令行那样pipeline式的插件流式处理(支持Go/WebAssembly)
三是内置一个Data Store用于集群控制和数据共享。

对于任何一个分布式系统,都需要有一个强一制性的基于Paxos/Raft的可以自动选主机制,并且需要在整个集群间同步一些关键的控制/配置和相关的共享数据,以保证整个集群的行为是统一一致的。如果没有这么一个东西的话,就没有办法玩分布式系统的。这就是为什么会有像Zookeeper/etcd这样的组件出现并流行的原因。注意,Zookeeper他们主要不是给你存数据的,而是给你组集群的。
Zookeeper是一个很流行的开源软件,也被用于各大公司的生产线,包括一些开源软件,比如:Kafka。但是,这会让其它软件有一个依赖,并且在运维上带来很大的复杂度。所以,Kafka在最新的版本也通过内置了选主的算法,而抛弃了外挂zookeeper的设计。Etcd是Go语言社区这边的主力,也是kubernetes组建集群的关键组件。Easegress在一开始(5年前)使用了gossip协议同步状态(当时想的过于超前,想做广域网的集群),但是后发现这个协议太过于复杂,而且很难调试,而广域网的API Gateway也没遇到相应的场景。所以,在3年前的时候,为了稳定性的考量,我们把其换成了内嵌版本的etcd,这个设计一直沿用到今天。
Easegress会把所有的配置信息都放到etcd里,还包括一些统计监控数据,以及一些用户的自定义数据(这样用户自己的plugin不但可以在一条pipeline内,还可以在整个集群内共享数据),这对于用户进行扩展来说是非常方便的。软件代码的扩展性一直是我们追求的首要目标,尤其是开源软件更要想方设法降低技术门槛让技术易扩展,这就是为什么Google的很多开源软件都会选使用Go语言的原因,也是为什么Go正在取代C/C++的做PaaS基础组件的原因。
背景问题
好了,在介绍完为什么要用etcd以后,我开始分享一个实际的问题了。我们有个用户在使用 Easegress 的时候,在Easegress内配置了上千条pipeline,导致 Easegress的内存飙升的非常厉害- 10+GB 以上,而且长时间还下不来。
用户报告的问题是——
在Easegress 1.4.1 上创建一个HTTP对象,1000个Pipeline,在Easegres初始化启动完成时的内存占用大概为400M,运行80分钟后2GB,运行200分钟后达到了4GB,这期间什么也没有干,对Easegress没有进行过一次请求。
一般来说,就算是API再多也不应该配置这么多的处理管道pipeline的,通常我们会使用HTTP API的前缀把一组属于一个类别的API配置在一个管道内是比较合理的,就像nginx下的location的配置,一般来说不会太多的。但是,在用户的这个场景下配置了上千个pipeline,我们也是头一次见,应该是用户想做更细粒度的控制。
经过调查后,我们发现内存使用基本全部来自etcd,我们实在没有想到,因为我们往etcd里放的数据也没有多少个key,感觉不会超过10M,但不知道为什么会占用了10GB的内存。这种时候,一般会怀疑etcd有内存泄漏,上etcd上的github上搜了一下,发现etcd在3.2和3.3的版本上都有内存泄露的问题,但都修改了,而 Easegress 使用的是3.5的最新版本,另外,一般来说内存泄漏的问题不会是这么大的,我们开始怀疑是我们哪里误用了etcd。要知道是否误用了etcd,那么只有一条路了,沉下心来,把etcd的设计好好地看一遍。

大概花了两天左右的时间看了一下etcd的设计,我发现了etcd有下面这些消耗内存的设计,老实说,还是非常昂贵的,这里分享出来,避免后面的同学再次掉坑。
首当其冲是——RaftLog。etcd用Raft Log,主要是用于帮助follower同步数据,这个log的底层实现不是文件,而是内存。所以,而且还至少要保留 5000 条最新的请求。如果key的size很大,这 5000条就会产生大量的内存开销。比如,不断更新一个 1M的key,哪怕是同一个key,这 5000 条Log就是 5000MB = 5GB 的内存开销。这个问题在etcd的issue列表中也有人提到过  issue #12548 ,不过,这个问题不了了之了。这个5000还是一个hardcode,无法改。(参看 DefaultSnapshotCatchUpEntries 相关源码)
// DefaultSnapshotCatchUpEntries is the number of entries for a slow follower
// to catch-up after compacting the raft storage entries.
// We expect the follower has a millisecond level latency with the leader.
// The max throughput is around 10K. Keep a 5K entries is enough for helping
// follower to catch up.
DefaultSnapshotCatchUpEntries uint64 = 5000
另外,我们还发现,这个设计在历史上etcd的官方团队把这个默认值从10000降到了5000,我们估计etcd官方团队也意识到10000有点太耗内存了,所以,降了一半,但是又怕follwer同步不上,所以,保留了 5000条……(在这里,我个人感觉还有更好的方法,至少不用全放在内存里吧……)
另外还有下面几项也会导致etcd的内存会增加

索引。etcd的每一对 key-value 都会在内存中有一个 B-tree 索引。这个索引的开销跟key的长度有关,etcd还会保存版本。所以B-tree的内存跟key的长度以及历史版本号数量也有关系。
mmap。还有,etcd 使用 mmap 这样上古的unix技术做文件映射,会把他的blotdb的内存map到虚拟内存中,所以,db-size越大,内存越大。
Watcher。watch也会占用很大的内存,如果watch很多,连接数多,都会堆积内存。

(很明显,etcd这么做就是为了一个高性能的考虑)

Easegress中的问题更多的应该是Raft Log 的问题。后面三种问题我们觉得不会是用户这个问题的原因,对于索引和mmap,使用 etcd 的 compact 和 defreg (压缩和碎片整理应该可以降低内存,但用户那边不应该是这个问题的核心原因)。
针对用户的问题,大约有1000多条pipeline,因为Easegress会对每一条pipeline进行数据统计(如:M1, M5, M15, P99, P90, P50等这样的统计数据),统计信息可能会有1KB-2KB左右,但Easegress会把这1000条pipeline的统计数据合并起来写到一个key中,这1000多条的统计数据合并后会导致出现一个平均尺寸为2MB的key,而5000个in-memory的RaftLog导致etcd要消耗了10GB的内存。之前没有这么多的pipeline的场景,所以,这个内存问题没有暴露出来。
于是,我们最终的解决方案也很简单,我们修改我们的策略,不再写这么大的Value的数据了,虽然以前只写在一个key上,但是Key的值太大,现在把这个大Key值拆分成多个小的key来写,这样,实际保存的数据没有发生变化,但是RaftLog的每条数据量就小了,所以,以前是5000条 2M(10GB),现在是5000条 1K(500MB),就这样解决了这个问题。相关的PR在这里 PR#542 。
总结
要用好 etcd,有如下的实践

避免大尺寸的key和value,一方面会通过一个内存级的 Raft Log 占大量内存,另一方面,B-tree的多版本索引也会因为这样耗内存。
避免DB的尺寸太大,并通过 compact和defreg来压缩和碎片整理降低内存。
避免大量的Watch Client 和 Watch数。这个开销也是比较大的。
最后还有一个,就是尽可能使用新的版本,无论是go语言还是etcd,这样会少很多内存问题。比如:golang的这个跟LInux内核心相关的内存问题 —— golang 1.12的版sget的是 MADV_FREE 的内存回收机制,而在1.16的时候,改成了 MADV_DONTNEED ,这两者的差别是,FREE表示,虽然进程标记内存不要了,但是操作系统会保留之,直到需要更多的内存,而 DONTNEED 则是立马回收,你可以看到,在常驻内存RSS 上,前者虽然在golang的进程上回收了内存,但是RSS值不变,而后者会看到RSS直立马变化。Linux下对 MADV_FREE 的实现在某些情况下有一定的问题,所以,在go 1.16的时候,默认值改成了 MADV_DONTNEED 。而 etcd 3.4 是用 来1.12 编译的。

最后,欢迎大家关注我们的开源软件! https://github.com/megaease/ 

(全文完)





-->
关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 &#8211; CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

相关文章Go编程模式 : 泛型编程Go 编程模式:k8s Visitor 模式Go编程模式:PipelineGo编程模式:委托和反转控制Go 编程模式:Go GenerationGo编程模式:Map-ReduceThe post ETCD的内存问题 first appeared on 酷 壳 - CoolShell.</content>
</doc>