Golang程序内存泄漏?

huangapple go评论71阅读模式
英文:

Golang program memory leak?

问题

我的golang程序(URL监控)存在内存泄漏问题,最终被内核杀死(oom)。
环境:

$ go version
go version go1.0.3

$ go env
GOARCH="amd64"
GOBIN=""
GOCHAR="6"
GOEXE=""
GOGCCFLAGS="-g -O2 -fPIC -m64 -pthread"
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/data/apps/go"
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
CGO_ENABLED="1"

代码:

package main
import (
"bytes"
"database/sql"
"flag"
"fmt"
_ "github.com/Go-SQL-Driver/MySQL"
"ijinshan.com/cfg"
"log"
"net"
"net/http"
"net/smtp"
"os"
"strconv"
"strings"
"sync"
"time"
)
var (
Log           *log.Logger
Conf          cfg.KVConfig
Debug         bool
CpuCore       int
HttpTransport = &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
deadline := time.Now().Add(30 * time.Second)
c, err := net.DialTimeout(netw, addr, 20*time.Second)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
DisableKeepAlives: true,
}
HttpClient = &http.Client{
Transport: HttpTransport,
}
WG            sync.WaitGroup
)
const (
LogFileFlag   = os.O_WRONLY | os.O_CREATE | os.O_APPEND
LogFileMode   = 0644
LogFlag       = log.LstdFlags | log.Lshortfile
GET_VIDEO_SQL = `SELECT B.Name, A.TSID, A.Chapter, A.ChapterNum, 
IFNULL(A.Website, ''), IFNULL(A.Descr, ''), 
IFNULL(A.VideoId, ''), IFNULL(AndroidWebURL, ''), IFNULL(IOSWebURL, ''), 
IFNULL(AndroidURL, ''), IFNULL(AndroidURL2, ''), IFNULL(IOSURL, '')
FROM Video A INNER JOIN TVS B ON A.TSID = B.ID LIMIT 200`
HtmlHead = `<table border=1 width=100% height=100%><tr><td>节目名
</td><td>tsid</td><td>章节</td><td>章节号</td><td>描述
</td><td>videoid</td><td>网站</td><td>地址</td></tr>`
HtmlTail = "</table>"
)
type videoInfo struct {
name          string
tsid          uint
chapter       string
chapterNum    uint
descr         string
videoId       string
website       string
androidWebUrl string
iosWebUrl     string
androidUrl    string
androidUrl2   string
iosUrl        string
}
func init() {
var (
confFile string
err      error
)
// parse command argument:w
flag.StringVar(&confFile, "c", "./vsmonitor.conf", " set config file path")
flag.Parse()
// read config
if Conf, err = cfg.Read(confFile); err != nil {
panic(fmt.Sprintf("Read config file \"%s\" failed (%s)",
confFile, err.Error()))
}
// open log file
file, err := os.OpenFile(Conf["log.file"], LogFileFlag, LogFileMode)
if err != nil {
panic(fmt.Sprintf("OpenFile \"%s\" failed (%s)", Conf["log.file"],
err.Error()))
}
// init LOG
Log = log.New(file, "", LogFlag)
Debug = false
i, err := strconv.ParseInt(Conf["cpucore.num"], 10, 32)
if err != nil {
panic(fmt.Sprintf("ParseInt \"%s\" failed (%s)", Conf["cpucore.num"],
err.Error()))
}
CpuCore = int(i)
}
func getHttpStatusCode(url string) int {
if url == "" {
return 200
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0
}
req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17")
req.Header.Add("Connection", "close")
resp, err := HttpClient.Do(req)
if err != nil {
return 0
}
defer resp.Body.Close()
return resp.StatusCode
}
func sendMail(host, user, pwd, from, to, subject, body, mailType string) error {
auth := smtp.PlainAuth("", user, pwd, strings.Split(host, ":")[0])
cntType := fmt.Sprintf("Content-Type: text/%s;charset=UTF-8", mailType)
msg := fmt.Sprintf("To: %s\r\nFrom: %s<%s>\r\nSubject: %s\r\n%s\r\n\r\n%s",
to, from, user, subject, cntType, body)
return smtp.SendMail(host, auth, user, strings.Split(to, ","), []byte(msg))
}
func getVideos(videoChan chan *videoInfo, htmlBuf *bytes.Buffer) error {
defer HttpTransport.CloseIdleConnections()
db, err := sql.Open("mysql", Conf["weikan.mysql"])
if err != nil {
return err
}
rows, err := db.Query(GET_VIDEO_SQL)
if err != nil {
db.Close()
return err
}
for rows.Next() {
video := &videoInfo{}
err = rows.Scan(&video.name, &video.tsid, &video.chapter,
&video.chapterNum,
&video.website, &video.descr, &video.videoId, &video.androidWebUrl,
&video.iosWebUrl, &video.androidUrl, &video.androidUrl2,
&video.iosUrl)
if err != nil {
db.Close()
return err
}
videoChan <- video
WG.Add(1)
}
db.Close()
// wait check url finish
WG.Wait()
// send mail
for {
if htmlBuf.Len() == 0 {
Log.Print("no error found!!!!!!!!")
break
}
Log.Print("found error !!!!!!!!")
/*
err := sendMail("smtp.gmail.com:587", "xxxx",
"xxx", "xxx <xxx>",
Conf["mail.to"], "xxxxx",
HtmlHead+htmlBuf.String()+HtmlTail, "html")
if err != nil {
Log.Printf("sendMail failed (%s)", err.Error())
time.Sleep(10 * time.Second)
continue
}
*/
Log.Print("send mail")
break
}
Log.Print("reset buf")
htmlBuf.Reset()
return nil
}
func checkUrl(videoChan chan *videoInfo, errChan chan string) {
defer func() {
if err := recover(); err != nil {
Log.Print("rouintes failed : ", err)
}
}()
for {
video := <-videoChan
ok := true
errUrl := ""
if code := getHttpStatusCode(video.androidWebUrl); code >= 400 {
errUrl += fmt.Sprintf("%s (%d)<br />",
video.androidWebUrl, code)
ok = false
}
if code := getHttpStatusCode(video.iosWebUrl); code >= 400 {
errUrl += fmt.Sprintf("%s (%d)<br />",
video.iosWebUrl, code)
ok = false
}
if code := getHttpStatusCode(video.androidUrl); code >= 400 {
errUrl += fmt.Sprintf("%s (%d)<br />",
video.androidUrl, code)
ok = false
}
if code := getHttpStatusCode(video.androidUrl2); code >= 400 {
errUrl += fmt.Sprintf("%s (%d)<br />",
video.androidUrl2, code)
ok = false
}
if code := getHttpStatusCode(video.iosUrl); code >= 400 {
errUrl += fmt.Sprintf("%s (%d)<br />",
video.iosUrl, code)
ok = false
}
if !ok {
errChan <- fmt.Sprintf(`<tr><td>%s</td><td>%d</td><td>%s</td>
<td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>`,
video.name, video.tsid, video.chapter, video.chapterNum,
video.descr, video.videoId,
video.website, errUrl)
Log.Printf("\"%s\" (%s) —— \"%s\" checkurl err", video.name,
video.chapter, video.descr)
} else {
Log.Printf("\"%s\" (%s) —— \"%s\" checkurl ok", video.name,
video.chapter, video.descr)
WG.Done()
}
}
}
func mergeErr(errChan chan string, htmlBuf *bytes.Buffer) {
defer func() {
if err := recover(); err != nil {
Log.Print("rouintes failed : ", err)
}
}()
for {
html := <-errChan
_, err := htmlBuf.WriteString(html)
if err != nil {
Log.Printf("htmlBuf WriteString \"%s\" failed (%s)", html,
err.Error())
panic(err)
}
WG.Done()
}
}
func main() {
videoChan := make(chan *videoInfo, 100000)
errChan := make(chan string, 100000)
htmlBuf := &bytes.Buffer{}
defer func() {
if err := recover(); err != nil {
Log.Print("rouintes failed : ", err)
}
}()
// check url
for i := 0; i < CpuCore; i++ {
go checkUrl(videoChan, errChan)
}
// merge error string then send mail
go mergeErr(errChan, htmlBuf)
for {
// get Video and LiveSrc video source
if err := getVideos(videoChan, htmlBuf); err != nil {
Log.Printf("getVideos failed (%s)", err.Error())
time.Sleep(10 * time.Second)
continue
}
// time.Sleep(1 * time.Hour)
}
Log.Print("exit...")
}

代码中有四个函数:

> getHttpStatusCode

释放资源使用resp.Body.Close()
> sendMail

我不需要手动释放资源

> mergeErr

通过使用htmlBuf(*bytes.Buffer)连接错误字符串
> getVideos

首先获取视频URL,然后将其发送到videoChan,然后等待所有的协程完成检查工作。
然后发送邮件并重置htmlBuf。

我没有找到需要释放的任何资源,但是。

> $ top

显示:

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                      
6451 root      20   0 3946m 115m 2808 S  0.7  0.2   6:11.20 vsmonitor

VIRT和RES会增长...

内存分析:

(pprof) top
Total: 10.8 MB
2.3  21.2%  21.2%      2.3  21.2% main.main
2.0  18.5%  39.8%      2.0  18.5% bufio.NewWriterSize
1.5  13.9%  53.7%      1.5  13.9% bufio.NewReaderSize
1.5  13.9%  67.6%      1.5  13.9% compress/flate.NewReader
0.5   4.6%  72.2%      0.5   4.6% net.newFD
0.5   4.6%  76.8%      0.5   4.6% net.sockaddrToTCP
0.5   4.6%  81.5%      4.5  41.7% net/http.(*Transport).getConn
0.5   4.6%  86.1%      2.5  23.2% net/http.(*persistConn).readLoop
0.5   4.6%  90.7%      0.5   4.6% net/textproto.(*Reader).ReadMIMEHeader
0.5   4.6%  95.4%      0.5   4.6% net/url.(*URL).ResolveReference
英文:

My golang program (url monitor) has a memory leak, it finially killed by kernel (oom).
the env:

$ go version
go version go1.0.3
$ go env
GOARCH=&quot;amd64&quot;
GOBIN=&quot;&quot;
GOCHAR=&quot;6&quot;
GOEXE=&quot;&quot;
GOGCCFLAGS=&quot;-g -O2 -fPIC -m64 -pthread&quot;
GOHOSTARCH=&quot;amd64&quot;
GOHOSTOS=&quot;linux&quot;
GOOS=&quot;linux&quot;
GOPATH=&quot;/data/apps/go&quot;
GOROOT=&quot;/usr/local/go&quot;
GOTOOLDIR=&quot;/usr/local/go/pkg/tool/linux_amd64&quot;
CGO_ENABLED=&quot;1&quot;

code:

package main
import (
&quot;bytes&quot;
&quot;database/sql&quot;
&quot;flag&quot;
&quot;fmt&quot;
_ &quot;github.com/Go-SQL-Driver/MySQL&quot;
&quot;ijinshan.com/cfg&quot;
&quot;log&quot;
&quot;net&quot;
&quot;net/http&quot;
&quot;net/smtp&quot;
&quot;os&quot;
&quot;strconv&quot;
&quot;strings&quot;
&quot;sync&quot;
&quot;time&quot;
)
var (
Log           *log.Logger
Conf          cfg.KVConfig
Debug         bool
CpuCore       int
HttpTransport = &amp;http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
deadline := time.Now().Add(30 * time.Second)
c, err := net.DialTimeout(netw, addr, 20*time.Second)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
DisableKeepAlives: true,
}
HttpClient = &amp;http.Client{
Transport: HttpTransport,
}
WG            sync.WaitGroup
)
const (
LogFileFlag   = os.O_WRONLY | os.O_CREATE | os.O_APPEND
LogFileMode   = 0644
LogFlag       = log.LstdFlags | log.Lshortfile
GET_VIDEO_SQL = `SELECT B.Name, A.TSID, A.Chapter, A.ChapterNum, 
IFNULL(A.Website, &#39;&#39;), IFNULL(A.Descr, &#39;&#39;), 
IFNULL(A.VideoId, &#39;&#39;), IFNULL(AndroidWebURL, &#39;&#39;), IFNULL(IOSWebURL, &#39;&#39;), 
IFNULL(AndroidURL, &#39;&#39;), IFNULL(AndroidURL2, &#39;&#39;), IFNULL(IOSURL, &#39;&#39;)
FROM Video A INNER JOIN TVS B ON A.TSID = B.ID LIMIT 200`
HtmlHead = `&lt;table border=1 width=100% height=100%&gt;&lt;tr&gt;&lt;td&gt;节目名
&lt;/td&gt;&lt;td&gt;tsid&lt;/td&gt;&lt;td&gt;章节&lt;/td&gt;&lt;td&gt;章节号&lt;/td&gt;&lt;td&gt;描述
&lt;/td&gt;&lt;td&gt;videoid&lt;/td&gt;&lt;td&gt;网站&lt;/td&gt;&lt;td&gt;地址&lt;/td&gt;&lt;/tr&gt;`
HtmlTail = &quot;&lt;/table&gt;&quot;
)
type videoInfo struct {
name          string
tsid          uint
chapter       string
chapterNum    uint
descr         string
videoId       string
website       string
androidWebUrl string
iosWebUrl     string
androidUrl    string
androidUrl2   string
iosUrl        string
}
func init() {
var (
confFile string
err      error
)
// parse command argument:w
flag.StringVar(&amp;confFile, &quot;c&quot;, &quot;./vsmonitor.conf&quot;, &quot; set config file path&quot;)
flag.Parse()
// read config
if Conf, err = cfg.Read(confFile); err != nil {
panic(fmt.Sprintf(&quot;Read config file \&quot;%s\&quot; failed (%s)&quot;,
confFile, err.Error()))
}
// open log file
file, err := os.OpenFile(Conf[&quot;log.file&quot;], LogFileFlag, LogFileMode)
if err != nil {
panic(fmt.Sprintf(&quot;OpenFile \&quot;%s\&quot; failed (%s)&quot;, Conf[&quot;log.file&quot;],
err.Error()))
}
// init LOG
Log = log.New(file, &quot;&quot;, LogFlag)
Debug = false
i, err := strconv.ParseInt(Conf[&quot;cpucore.num&quot;], 10, 32)
if err != nil {
panic(fmt.Sprintf(&quot;ParseInt \&quot;%s\&quot; failed (%s)&quot;, Conf[&quot;cpucore.num&quot;],
err.Error()))
}
CpuCore = int(i)
}
func getHttpStatusCode(url string) int {
if url == &quot;&quot; {
return 200
}
req, err := http.NewRequest(&quot;GET&quot;, url, nil)
if err != nil {
return 0
}
req.Header.Add(&quot;User-Agent&quot;, &quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17&quot;)
req.Header.Add(&quot;Connection&quot;, &quot;close&quot;)
resp, err := HttpClient.Do(req)
if err != nil {
return 0
}
defer resp.Body.Close()
return resp.StatusCode
}
func sendMail(host, user, pwd, from, to, subject, body, mailType string) error {
auth := smtp.PlainAuth(&quot;&quot;, user, pwd, strings.Split(host, &quot;:&quot;)[0])
cntType := fmt.Sprintf(&quot;Content-Type: text/%s;charset=UTF-8&quot;, mailType)
msg := fmt.Sprintf(&quot;To: %s\r\nFrom: %s&lt;%s&gt;\r\nSubject: %s\r\n%s\r\n\r\n%s&quot;,
to, from, user, subject, cntType, body)
return smtp.SendMail(host, auth, user, strings.Split(to, &quot;,&quot;), []byte(msg))
}
func getVideos(videoChan chan *videoInfo, htmlBuf *bytes.Buffer) error {
defer HttpTransport.CloseIdleConnections()
db, err := sql.Open(&quot;mysql&quot;, Conf[&quot;weikan.mysql&quot;])
if err != nil {
return err
}
rows, err := db.Query(GET_VIDEO_SQL)
if err != nil {
db.Close()
return err
}
for rows.Next() {
video := &amp;videoInfo{}
err = rows.Scan(&amp;video.name, &amp;video.tsid, &amp;video.chapter,
&amp;video.chapterNum,
&amp;video.website, &amp;video.descr, &amp;video.videoId, &amp;video.androidWebUrl,
&amp;video.iosWebUrl, &amp;video.androidUrl, &amp;video.androidUrl2,
&amp;video.iosUrl)
if err != nil {
db.Close()
return err
}
videoChan &lt;- video
WG.Add(1)
}
db.Close()
// wait check url finish
WG.Wait()
// send mail
for {
if htmlBuf.Len() == 0 {
Log.Print(&quot;no error found!!!!!!!!&quot;)
break
}
Log.Print(&quot;found error !!!!!!!!&quot;)
/*
err := sendMail(&quot;smtp.gmail.com:587&quot;, &quot;xxxx&quot;,
&quot;xxx&quot;, &quot;xxx &lt;xxx&gt;&quot;,
Conf[&quot;mail.to&quot;], &quot;xxxxx&quot;,
HtmlHead+htmlBuf.String()+HtmlTail, &quot;html&quot;)
if err != nil {
Log.Printf(&quot;sendMail failed (%s)&quot;, err.Error())
time.Sleep(10 * time.Second)
continue
}
*/
Log.Print(&quot;send mail&quot;)
break
}
Log.Print(&quot;reset buf&quot;)
htmlBuf.Reset()
return nil
}
func checkUrl(videoChan chan *videoInfo, errChan chan string) {
defer func() {
if err := recover(); err != nil {
Log.Print(&quot;rouintes failed : &quot;, err)
}
}()
for {
video := &lt;-videoChan
ok := true
errUrl := &quot;&quot;
if code := getHttpStatusCode(video.androidWebUrl); code &gt;= 400 {
errUrl += fmt.Sprintf(&quot;%s (%d)&lt;br /&gt;&quot;,
video.androidWebUrl, code)
ok = false
}
if code := getHttpStatusCode(video.iosWebUrl); code &gt;= 400 {
errUrl += fmt.Sprintf(&quot;%s (%d)&lt;br /&gt;&quot;,
video.iosWebUrl, code)
ok = false
}
if code := getHttpStatusCode(video.androidUrl); code &gt;= 400 {
errUrl += fmt.Sprintf(&quot;%s (%d)&lt;br /&gt;&quot;,
video.androidUrl, code)
ok = false
}
if code := getHttpStatusCode(video.androidUrl2); code &gt;= 400 {
errUrl += fmt.Sprintf(&quot;%s (%d)&lt;br /&gt;&quot;,
video.androidUrl2, code)
ok = false
}
if code := getHttpStatusCode(video.iosUrl); code &gt;= 400 {
errUrl += fmt.Sprintf(&quot;%s (%d)&lt;br /&gt;&quot;,
video.iosUrl, code)
ok = false
}
if !ok {
errChan &lt;- fmt.Sprintf(`&lt;tr&gt;&lt;td&gt;%s&lt;/td&gt;&lt;td&gt;%d&lt;/td&gt;&lt;td&gt;%s&lt;/td&gt;
&lt;td&gt;%d&lt;/td&gt;&lt;td&gt;%s&lt;/td&gt;&lt;td&gt;%s&lt;/td&gt;&lt;td&gt;%s&lt;/td&gt;&lt;td&gt;%s&lt;/td&gt;&lt;/tr&gt;`,
video.name, video.tsid, video.chapter, video.chapterNum,
video.descr, video.videoId,
video.website, errUrl)
Log.Printf(&quot;\&quot;%s\&quot; (%s) —— \&quot;%s\&quot; checkurl err&quot;, video.name,
video.chapter, video.descr)
} else {
Log.Printf(&quot;\&quot;%s\&quot; (%s) —— \&quot;%s\&quot; checkurl ok&quot;, video.name,
video.chapter, video.descr)
WG.Done()
}
}
}
func mergeErr(errChan chan string, htmlBuf *bytes.Buffer) {
defer func() {
if err := recover(); err != nil {
Log.Print(&quot;rouintes failed : &quot;, err)
}
}()
for {
html := &lt;-errChan
_, err := htmlBuf.WriteString(html)
if err != nil {
Log.Printf(&quot;htmlBuf WriteString \&quot;%s\&quot; failed (%s)&quot;, html,
err.Error())
panic(err)
}
WG.Done()
}
}
func main() {
videoChan := make(chan *videoInfo, 100000)
errChan := make(chan string, 100000)
htmlBuf := &amp;bytes.Buffer{}
defer func() {
if err := recover(); err != nil {
Log.Print(&quot;rouintes failed : &quot;, err)
}
}()
// check url
for i := 0; i &lt; CpuCore; i++ {
go checkUrl(videoChan, errChan)
}
// merge error string then send mail
go mergeErr(errChan, htmlBuf)
for {
// get Video and LiveSrc video source
if err := getVideos(videoChan, htmlBuf); err != nil {
Log.Printf(&quot;getVideos failed (%s)&quot;, err.Error())
time.Sleep(10 * time.Second)
continue
}
// time.Sleep(1 * time.Hour)
}
Log.Print(&quot;exit...&quot;)
}

the code has four funcs:

> getHttpStatusCode

free resource use resp.Body.Close()
> sendMail

I don't need to free the resource manually

> mergeErr

concat the err string by using a htmlBuf(*bytes.Buffer)
> getVideos

First it gets the Video urls and then sends them to videoChan then it waits all the routines finish their check jobs.
then sendmail and reset htmlBuf.

I don't find any resource that needs free, but.

> $ top

shows:

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                      
6451 root      20   0 3946m 115m 2808 S  0.7  0.2   6:11.20 vsmonitor

the VIRT and RES will grow ...

memory profiling:

(pprof) top
Total: 10.8 MB
2.3  21.2%  21.2%      2.3  21.2% main.main
2.0  18.5%  39.8%      2.0  18.5% bufio.NewWriterSize
1.5  13.9%  53.7%      1.5  13.9% bufio.NewReaderSize
1.5  13.9%  67.6%      1.5  13.9% compress/flate.NewReader
0.5   4.6%  72.2%      0.5   4.6% net.newFD
0.5   4.6%  76.8%      0.5   4.6% net.sockaddrToTCP
0.5   4.6%  81.5%      4.5  41.7% net/http.(*Transport).getConn
0.5   4.6%  86.1%      2.5  23.2% net/http.(*persistConn).readLoop
0.5   4.6%  90.7%      0.5   4.6% net/textproto.(*Reader).ReadMIMEHeader
0.5   4.6%  95.4%      0.5   4.6% net/url.(*URL).ResolveReference

答案1

得分: 9

很容易在你的程序中添加一个选项,以便记录内存的使用情况。在你的程序中,我没有发现什么明显的错误。你下载的文件很大吗?你可以尝试使用HEAD请求吗?我不知道这是否有帮助;如果你有大量的请求,也许会有所帮助。

关于内存分析,Go博客上有一篇(有点旧的)文章,链接为http://blog.golang.org/2011/06/profiling-go-programs.html,还有相关的文档链接为http://golang.org/pkg/runtime/pprof/和http://golang.org/pkg/net/http/pprof/。

英文:

It's pretty easy to add an option to your program so it'll record where the memory is being used. Nothing stood out to me in your program as terribly wrong. Are the files you download very large? Could you do a HEAD request instead? I've no idea if that'd help; if you have a high volume of requests maybe it would.

There is an (old-ish) article on the Go blog about the memory profiling at http://blog.golang.org/2011/06/profiling-go-programs.html and documentation at http://golang.org/pkg/runtime/pprof/ and http://golang.org/pkg/net/http/pprof/

huangapple
  • 本文由 发表于 2013年3月28日 13:13:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/15674709.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定