NVR ร้องไม่หยุด แต่ตอนดิสก์พังจริงกลับเงียบสนิท
ปุ่มปิดเสียงเป็นปุ่มที่อันตรายที่สุดบนหน้า NVR เพราะมันไม่ได้ปิดแค่เสียงที่น่ารำคาญ มันปิดเสียงที่จะเตือนเราว่าดิสก์กำลังจะพังไปพร้อมกันด้วย และเราจะรู้ตัวอีกทีตอนที่ต้องใช้ภาพย้อนหลังแล้วไม่มี
พอดีได้เรื่องมาว่า NVR ตัวหนึ่งร้องแทบทั้งวัน เสียงเตือนดังเป็นชุด ๆ จนคนที่นั่งอยู่ใกล้ทนไม่ไหว ช่างที่ไปดูก่อนหน้าสรุปว่าเป็น videoloss เพราะกล้องหลุดบ่อย แล้วก็เข้าเมนูไปปิดเสียงเตือนทิ้งทั้งเครื่อง เรื่องจบไปสองเดือน จนวันที่ต้องขอภาพย้อนหลังแล้วพบว่าฮาร์ดดิสก์สองลูกขึ้นสถานะ Exception พร้อมกันมาตั้งแต่เมื่อไหร่ก็ไม่มีใครรู้ เพราะมันไม่เคยส่งเสียงอะไรเลย ผมเลยไปรื้อดูทั้งชุดตั้งแต่ว่าเสียงที่ร้องคืออะไรจริง ๆ แล้วแก้ที่นโยบายการแจ้งเตือนแทนการกดปิด
เลข IP ในบทความเป็นค่าตัวอย่าง ส่วนคำสั่งกับโครงสร้าง XML เป็นของจริง และทั้งหมดทำกับอุปกรณ์ที่เราดูแลอยู่ด้วยบัญชีผู้ดูแลที่ได้รับมอบมาพร้อมงาน
เสียงที่ร้อง กับเหตุการณ์ที่เกิด เป็นคนละเรื่องกัน
เสียงเตือนบน NVR เป็นเสียงเดียว ไม่ว่าจะเกิดจากสาเหตุอะไร คนที่ยืนฟังอยู่หน้าเครื่องไม่มีทางแยกออกว่าเป็นกล้องหลุด ดิสก์เสีย หรือ IP ชน ทุกคนเลยเดาเอาจากสิ่งที่เห็นบนจอ ซึ่งตอนนั้นมีช่องกระพริบอยู่พอดี ก็สรุปกันว่า videoloss
ของจริงต้องไปอ่านจากบันทึกเหตุการณ์ในตัวเครื่อง ไม่ใช่เดาจากเสียง พอไล่ดูย้อนหลังปรากฏว่ารายการที่ถี่ที่สุดคือ ipconflict ไม่ใช่ videoloss ส่วน videoloss มีจริงแต่เป็นผลพวงตามมา เพราะตอน IP ชนกัน NVR ก็ต่อกล้องตัวนั้นไม่ได้อยู่แล้ว
เสียงบัซเซอร์บอกแค่ว่า "มีบางอย่างเกิด" ไม่ได้บอกว่าอะไรเกิด การเดาจากภาพบนจอตอนนั้นทำให้ทีมแก้ผิดจุดไปสองเดือน ไปไล่เปลี่ยนสายกับพอร์ตสวิตช์ทั้งที่ต้นเหตุอยู่ที่การแจก IP เสียเวลาและค่าอะไหล่ไปฟรี ๆ
อ่าน trigger ทั้งหมดออกมาก่อน อย่าเพิ่งไปกดที่หน้าจอ
หน้าเมนูของ NVR ให้ปิดเสียงได้แบบเดียวคือปิดทั้งเครื่อง แต่ฝั่ง ISAPI ให้ความละเอียดกว่านั้นมาก เพราะมันมองทุกอย่างเป็น trigger แยกกันทีละเหตุการณ์ทีละช่อง แต่ละตัวมีรายการช่องทางแจ้งเตือนของตัวเองว่าจะให้ส่งเสียง ให้บันทึก ให้ยิงไปที่ศูนย์ หรือให้ส่งเมล
export NVR_HOST=10.10.10.20
export NVR_USER=admin
export NVR_PASS='<PASSWORD>'
curl -s --digest -u "$NVR_USER:$NVR_PASS" \
"http://$NVR_HOST/ISAPI/Event/triggers" | head -40
ที่ได้กลับมาคือรายการยาวมาก เพราะมันไล่ทุกเหตุการณ์คูณทุกช่อง ผมเลยดึงมาเป็นไฟล์แล้วกรองเอาเฉพาะชื่อ trigger ออกมาดูก่อนว่ามีอะไรบ้าง
curl -s --digest -u "$NVR_USER:$NVR_PASS" \
"http://$NVR_HOST/ISAPI/Event/triggers" \
| grep -o '<id>[^<]*</id>' | sed 's/<[^>]*>//g' | sort | uniq
ชื่อที่โผล่มาจะเป็นรูปแบบ <eventType>-<channel> เช่น videoloss-1 ipconflict-1 diskerror-1 diskfull-1 recordingfailure-1 illaccess-1 ตัวที่ผมสนใจในงานนี้มีไม่กี่ตัว แต่ต้องเห็นทั้งหมดก่อนถึงจะรู้ว่าเสียงมาจากไหนได้บ้าง
curl -s --digest -u "$NVR_USER:$NVR_PASS" \
"http://$NVR_HOST/ISAPI/Event/triggers/ipconflict-1"
<EventTrigger version="2.0" xmlns="http://www.hikvision.com/ver20/XMLSchema">
<id>ipconflict-1</id>
<eventType>ipconflict</eventType>
<eventDescription>ipconflict Event trigger</eventDescription>
<EventTriggerNotificationList>
<EventTriggerNotification>
<id>beep</id>
<notificationMethod>beep</notificationMethod>
<notificationRecurrence>beginning</notificationRecurrence>
</EventTriggerNotification>
<EventTriggerNotification>
<id>center</id>
<notificationMethod>center</notificationMethod>
<notificationRecurrence>beginning</notificationRecurrence>
</EventTriggerNotification>
</EventTriggerNotificationList>
</EventTrigger>
เห็น beep อยู่ในรายการนี้คือคำตอบของคำถามว่าเสียงมาจากไหน ส่วน center คือส่งไปที่ซอฟต์แวร์ศูนย์กลาง ตัวนี้ผมเก็บไว้ เพราะมันไม่ได้กวนใคร แล้วเป็นสิ่งที่เราอยากได้ไว้ดูย้อนหลัง
แก้ที่ policy: ถอด beep ทีละ event ไม่ใช่ปิดยกเครื่อง
วิธีแก้คือ PUT ก้อน XML เดิมกลับไปโดยตัดเฉพาะบล็อก beep ออก ช่องทางอื่นคงไว้ทั้งหมด ทำทีละ trigger ที่เราตัดสินใจแล้วว่าไม่คุ้มกับการส่งเสียง
curl -s --digest -u "$NVR_USER:$NVR_PASS" -X PUT \
"http://$NVR_HOST/ISAPI/Event/triggers/ipconflict-1" \
-H 'Content-Type: application/xml' \
--data-binary @ipconflict-1-nobeep.xml
ทำทีละตัวด้วยมือสำหรับเครื่องเดียวยังพอไหว แต่ถ้ามีหลายเครื่องควรเขียนสคริปต์ ผมใช้แบบนี้ อ่านของเดิมมา ถอดบล็อกที่มี beep ออกด้วยการตัดตามคู่แท็ก แล้วส่งกลับ
import os, re, requests
from requests.auth import HTTPDigestAuth
B = "http://%s/ISAPI" % os.environ["NVR_HOST"]
A = HTTPDigestAuth(os.environ["NVR_USER"], os.environ["NVR_PASS"])
BEEP = re.compile(r"\s*<EventTriggerNotification>(?:(?!</EventTriggerNotification>).)*"
r"<notificationMethod>beep</notificationMethod>.*?"
r"</EventTriggerNotification>", re.S)
def set_beep(trigger_id, want_beep):
xml = requests.get("%s/Event/triggers/%s" % (B, trigger_id), auth=A, timeout=8).text
has = "<notificationMethod>beep</notificationMethod>" in xml
if has == want_beep:
return "unchanged"
if has and not want_beep:
new = BEEP.sub("", xml)
else:
blk = ("<EventTriggerNotification><id>beep</id>"
"<notificationMethod>beep</notificationMethod>"
"<notificationRecurrence>beginning</notificationRecurrence>"
"</EventTriggerNotification>")
new = xml.replace("<EventTriggerNotificationList>",
"<EventTriggerNotificationList>" + blk, 1)
r = requests.put("%s/Event/triggers/%s" % (B, trigger_id), auth=A,
data=new.encode("utf-8"),
headers={"Content-Type": "application/xml"}, timeout=8)
return "ok" if "<statusCode>1</statusCode>" in r.text else r.text[:120]
นโยบายที่ผมตั้งไว้หลังจากนั้นคือให้ส่งเสียงเฉพาะเหตุการณ์ที่แปลว่า "ภาพกำลังหาย" เท่านั้น เหลือสองตัวคือ diskerror กับ recordingfailure ส่วนที่เหลือให้บันทึกและยิงเข้าศูนย์ แต่ไม่ส่งเสียง
SILENT = ["ipconflict-1", "videoloss-1", "videoloss-2", "illaccess-1", "nettbroken-1"]
LOUD = ["diskerror-1", "diskfull-1", "recordingfailure-1"]
for t in SILENT:
print(t, set_beep(t, False))
for t in LOUD:
print(t, set_beep(t, True))
เงียบทั้งเครื่อง จบใน 10 วินาที
แก้อาการรำคาญได้ทันที แต่ปิดทุกอย่างรวมถึงเหตุการณ์ที่เราอยากรู้ที่สุด และไม่มีร่องรอยว่าใครปิดเมื่อไหร่ คนที่มาดูต่อในอีกหกเดือนจะไม่มีทางรู้ว่าเครื่องนี้ถูกปิดปากไว้
เงียบเฉพาะที่ตั้งใจ
ใช้เวลาเพิ่มอีกครึ่งชั่วโมง แต่ได้ไฟล์ XML ของทุก trigger เก็บไว้เป็นเอกสาร รู้ชัดว่าอะไรยังร้องอยู่ รันสคริปต์เดิมซ้ำกับเครื่องตัวถัดไปได้ และตรวจย้อนได้ทุกเมื่อว่านโยบายยังตรงไหม
ต้นเหตุจริงคือกล้องเป็น DHCP แล้ว IP ชนกันเอง
ปิดเสียงเป็นแค่การกลบอาการ ต้นเหตุยังต้องแก้ กล้องในระบบนี้ตั้งเป็น DHCP ทั้งหมดตอนติดตั้ง แล้วช่วง IP ที่เราเตอร์แจกซ้อนทับกับ IP ที่มีคนไปตั้งแบบ static ให้อุปกรณ์อื่นทีหลัง พอเราเตอร์รีบูตหรือ lease หมดอายุพร้อมกันหลายตัว ก็มีโอกาสแจกไปชนกับของเดิม
วิธีแก้ที่ผมใช้คือไม่เปลี่ยนกล้องเป็น static เพราะการไล่ตั้ง static ทีละตัวบนบันไดคือสิ่งที่ไม่มีใครอยากทำซ้ำ แต่ไปจอง IP ตาม MAC ที่เราเตอร์แทน กล้องยังเป็น DHCP เหมือนเดิม แค่ได้เลขเดิมทุกครั้ง แล้วหดช่วงที่แจกอิสระให้เล็กลงจนไม่ทับกับช่วงที่ใครจะไปตั้ง static
ราคาที่จ่ายตอนปิดเสียงทั้งหมด
ช่วงสองเดือนที่เครื่องถูกปิดเสียงไว้ ฮาร์ดดิสก์ทั้งสองลูกขึ้นสถานะ Exception ไล่กันไม่ห่างกันมาก ผลคือช่วงเวลาที่ควรจะมีภาพย้อนหลัง หายไปทั้งก้อน ไม่มีใครรู้จนถึงวันที่ต้องเปิดดู
สาเหตุที่ดิสก์สองลูกพังใกล้กันไม่ใช่เรื่องบังเอิญนัก ดิสก์ในเครื่องบันทึกมักซื้อพร้อมกัน ล็อตเดียวกัน เขียนตลอดยี่สิบสี่ชั่วโมงเท่ากัน อายุการใช้งานเลยมาถึงไล่ ๆ กัน ใครที่คิดว่ามีสองลูกแล้วปลอดภัยกว่าลูกเดียว ต้องคิดใหม่ถ้าสองลูกนั้นมาจากกล่องเดียวกัน
เครื่องบันทึกเขียนดิสก์ตลอดเวลาไม่มีพัก ดิสก์ที่ซื้อพร้อมกันและรับภาระเท่ากันจะเดินเข้าเส้นตายใกล้กันมาก การมีหลายลูกเลยไม่ใช่ความปลอดภัยถ้าไม่มีคนคอยดูสถานะ สิ่งที่ป้องกันการสูญเสียภาพได้จริงคือการเฝ้าสถานะดิสก์ ไม่ใช่จำนวนลูก
เฝ้าสถานะดิสก์เองด้วย ISAPI ไม่ต้องรอเสียง
หลังจากเจอเรื่องนี้ ผมเลิกพึ่งเสียงจากตัวเครื่องเป็นด่านหลัก แล้วเขียนตัวเฝ้าสถานะดิสก์ยิงถามเองทุกชั่วโมงแทน endpoint คือ /ISAPI/ContentMgmt/Storage คืนสถานะทุกลูกพร้อมความจุและพื้นที่เหลือ
curl -s --digest -u "$NVR_USER:$NVR_PASS" \
"http://$NVR_HOST/ISAPI/ContentMgmt/Storage" \
| grep -E '<(id|status|capacity|freeSpace|hddName)>'
import re, requests
def hdd_status():
xml = requests.get(B + "/ContentMgmt/Storage", auth=A, timeout=8).text
out = []
for blk in re.findall(r"<hdd>(.*?)</hdd>", xml, re.S):
g = lambda t: (re.search("<%s>([^<]*)</%s>" % (t, t), blk) or [None, ""])[1]
out.append(dict(id=g("id"), name=g("hddName"), status=g("status"),
cap=int(g("capacity") or 0), free=int(g("freeSpace") or 0)))
return out
for h in hdd_status():
# ok / unformatted / error / mismatch / sleeping — อะไรที่ไม่ใช่ ok คือต้องแจ้งคน
if h["status"] != "ok":
print("ALERT hdd", h["id"], h["name"], h["status"])
ตัวนี้ผมให้รันบนเครื่องเล็ก ๆ ในวงเดียวกัน แล้วยิงแจ้งเตือนออกไปทางช่องทางที่คนอ่านจริง ข้อดีคือมันแยกอิสระจากตัว NVR ถ้าวันหนึ่ง NVR เองไม่ตอบ เราก็รู้ทันทีเหมือนกัน ซึ่งเป็นเหตุการณ์ที่บัซเซอร์บนตัวเครื่องไม่มีวันบอกเราได้อยู่แล้ว
กับดัก H.265 ที่ทำให้ตัวเลขเก็บได้กี่วันผิด
เรื่องสุดท้ายที่เจอในงานเดียวกัน ตอนคำนวณว่าดิสก์ที่มีเก็บภาพได้กี่วัน ทุกคนอ่านจากหน้า NVR ว่ากล้องเป็น H.265 แล้วคิดอัตราการกินพื้นที่ตามนั้น ปรากฏว่าไม่ตรง พอไปถามที่ตัวกล้องตรง ๆ กลับพบว่ากล้องที่ต่อเข้ามาแบบ ONVIF หลายตัวยังส่ง H.264 ออกมาจริง ๆ ค่าที่หน้า NVR โชว์เป็นค่าที่ NVR ขอไว้ตอนเพิ่มช่อง ไม่ใช่ค่าที่ไหลอยู่
# ถามที่ตัวกล้องเอง ไม่ใช่ดูจากหน้า NVR
curl -s --digest -u "admin:$CAM_PASS" \
"http://10.10.10.31/ISAPI/Streaming/channels/101" \
| grep -E '<(videoCodecType|videoResolutionWidth|constantBitRate|maxFrameRate)>'
ผลของการเชื่อผิดคือตัวเลขวันที่เก็บได้ผิดไปเกือบเท่าตัว เพราะ H.264 กินพื้นที่ประมาณสองเท่าที่ความละเอียดและคุณภาพเดียวกัน วิธีคิดที่ผมใช้คือไม่เดาจาก codec เลย แต่เอาบิตเรตจริงมาคูณตรง ๆ
คิดง่าย ๆ คือบิตเรตหน่วยเมกะบิตต่อวินาที หารแปดได้เมกะไบต์ต่อวินาที คูณแปดหมื่นหกพันสี่ร้อยวินาทีได้ต่อวันต่อช่อง แล้วคูณจำนวนช่อง สี่เมกะบิตต่อวินาทีคือราวสี่สิบสองกิกะไบต์ต่อวันต่อช่อง สิบหกช่องคือหกร้อยเจ็ดสิบห้ากิกะไบต์ต่อวัน พื้นที่ใช้งานจริงสิบสองเทระไบต์เลยเก็บได้ราวสิบแปดวัน ไม่ใช่สามสิบหกวันตามที่เคยบอกลูกค้าไว้
นโยบายเสียงและการเฝ้าระวังที่ผมใช้อยู่
- ก่อนแตะอะไร ดึงก้อน XML ของทุก trigger ออกมาเก็บเป็นไฟล์ไว้ก่อน ได้ทั้งจุดตั้งต้นสำหรับย้อนกลับ และเอกสารของระบบไปในตัว
- ไล่ดูรายการเหตุการณ์ย้อนหลังก่อนตัดสินว่าเสียงมาจากอะไร อย่าเดาจากสิ่งที่เห็นบนจอตอนนั้น
- ถอด beep เฉพาะ trigger ที่เราตัดสินใจแล้วว่าไม่คุ้ม เก็บช่องทางบันทึกกับส่งเข้าศูนย์ไว้เสมอ
- เหลือเสียงไว้เฉพาะเหตุการณ์ที่แปลว่าภาพกำลังหาย คือดิสก์ผิดปกติ กับบันทึกไม่สำเร็จ
- แก้ต้นเหตุด้วยการจอง IP ตาม MAC แล้วหดช่วงที่ DHCP แจกอิสระ ไม่ต้องไล่ตั้ง static ทีละตัว
- วางตัวเฝ้าสถานะดิสก์แยกออกมาจาก NVR ยิงถามทุกชั่วโมง และให้มันร้องด้วยเมื่อ NVR เองไม่ตอบ
- คำนวณวันที่เก็บได้จากบิตเรตจริงที่อ่านจากตัวกล้อง ไม่ใช่จาก codec ที่หน้า NVR โชว์
ส่วนตัวผมคิดว่าปัญหาของระบบกล้องส่วนใหญ่ไม่ใช่เรื่องอุปกรณ์ไม่ดี แต่เป็นเรื่องที่ระบบส่งเสียงเยอะเกินจนคนเลิกฟัง แล้วพอเลิกฟังแล้วเราก็เหลือระบบที่ไม่มีใครเฝ้าเลย การแก้ที่ถูกคือลดเสียงให้เหลือเฉพาะเรื่องที่ต้องลุกไปทำจริง ไม่ใช่ปิดทั้งหมดแล้วหวังว่าจะไม่มีอะไรเกิดขึ้น ใครจะเอานโยบายชุดนี้ไปปรับให้เข้ากับหน้างานตัวเอง เปลี่ยนรายการ SILENT กับ LOUD ได้ตามสะดวก แต่อย่าให้รายการ LOUD ว่างเปล่า
ที่ยังค้างในแผนของผมคือเก็บก้อน XML ของทุก trigger ลง git เพื่อให้เห็นว่ามีใครไปเปลี่ยนนโยบายตอนไหนบ้าง กับทำตัวตรวจเทียบอัตโนมัติว่าค่าบนเครื่องยังตรงกับนโยบายที่ตกลงกันไว้หรือเปล่า เพราะหลังจากช่างเข้าไปทำอย่างอื่น ค่าพวกนี้กลับไปเป็นค่าเดิมได้เสมอ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย