Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 
 
 

243 Zeilen
7.8 KiB

  1. # Copyright 2021 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import ctypes
  15. import logging
  16. import os
  17. import re
  18. from typing import Iterable, Optional, overload
  19. import attr
  20. from prometheus_client import REGISTRY, Metric
  21. from typing_extensions import Literal
  22. from synapse.metrics import GaugeMetricFamily
  23. from synapse.metrics._types import Collector
  24. logger = logging.getLogger(__name__)
  25. @attr.s(slots=True, frozen=True, auto_attribs=True)
  26. class JemallocStats:
  27. jemalloc: ctypes.CDLL
  28. @overload
  29. def _mallctl(
  30. self, name: str, read: Literal[True] = True, write: Optional[int] = None
  31. ) -> int:
  32. ...
  33. @overload
  34. def _mallctl(
  35. self, name: str, read: Literal[False], write: Optional[int] = None
  36. ) -> None:
  37. ...
  38. def _mallctl(
  39. self, name: str, read: bool = True, write: Optional[int] = None
  40. ) -> Optional[int]:
  41. """Wrapper around `mallctl` for reading and writing integers to
  42. jemalloc.
  43. Args:
  44. name: The name of the option to read from/write to.
  45. read: Whether to try and read the value.
  46. write: The value to write, if given.
  47. Returns:
  48. The value read if `read` is True, otherwise None.
  49. Raises:
  50. An exception if `mallctl` returns a non-zero error code.
  51. """
  52. input_var = None
  53. input_var_ref = None
  54. input_len_ref = None
  55. if read:
  56. input_var = ctypes.c_size_t(0)
  57. input_len = ctypes.c_size_t(ctypes.sizeof(input_var))
  58. input_var_ref = ctypes.byref(input_var)
  59. input_len_ref = ctypes.byref(input_len)
  60. write_var_ref = None
  61. write_len = ctypes.c_size_t(0)
  62. if write is not None:
  63. write_var = ctypes.c_size_t(write)
  64. write_len = ctypes.c_size_t(ctypes.sizeof(write_var))
  65. write_var_ref = ctypes.byref(write_var)
  66. # The interface is:
  67. #
  68. # int mallctl(
  69. # const char *name,
  70. # void *oldp,
  71. # size_t *oldlenp,
  72. # void *newp,
  73. # size_t newlen
  74. # )
  75. #
  76. # Where oldp/oldlenp is a buffer where the old value will be written to
  77. # (if not null), and newp/newlen is the buffer with the new value to set
  78. # (if not null). Note that they're all references *except* newlen.
  79. result = self.jemalloc.mallctl(
  80. name.encode("ascii"),
  81. input_var_ref,
  82. input_len_ref,
  83. write_var_ref,
  84. write_len,
  85. )
  86. if result != 0:
  87. raise Exception("Failed to call mallctl")
  88. if input_var is None:
  89. return None
  90. return input_var.value
  91. def refresh_stats(self) -> None:
  92. """Request that jemalloc updates its internal statistics. This needs to
  93. be called before querying for stats, otherwise it will return stale
  94. values.
  95. """
  96. try:
  97. self._mallctl("epoch", read=False, write=1)
  98. except Exception as e:
  99. logger.warning("Failed to reload jemalloc stats: %s", e)
  100. def get_stat(self, name: str) -> int:
  101. """Request the stat of the given name at the time of the last
  102. `refresh_stats` call. This may throw if we fail to read
  103. the stat.
  104. """
  105. return self._mallctl(f"stats.{name}")
  106. _JEMALLOC_STATS: Optional[JemallocStats] = None
  107. def get_jemalloc_stats() -> Optional[JemallocStats]:
  108. """Returns an interface to jemalloc, if it is being used.
  109. Note that this will always return None until `setup_jemalloc_stats` has been
  110. called.
  111. """
  112. return _JEMALLOC_STATS
  113. def _setup_jemalloc_stats() -> None:
  114. """Checks to see if jemalloc is loaded, and hooks up a collector to record
  115. statistics exposed by jemalloc.
  116. """
  117. global _JEMALLOC_STATS
  118. # Try to find the loaded jemalloc shared library, if any. We need to
  119. # introspect into what is loaded, rather than loading whatever is on the
  120. # path, as if we load a *different* jemalloc version things will seg fault.
  121. # We look in `/proc/self/maps`, which only exists on linux.
  122. if not os.path.exists("/proc/self/maps"):
  123. logger.debug("Not looking for jemalloc as no /proc/self/maps exist")
  124. return
  125. # We're looking for a path at the end of the line that includes
  126. # "libjemalloc".
  127. regex = re.compile(r"/\S+/libjemalloc.*$")
  128. jemalloc_path = None
  129. with open("/proc/self/maps") as f:
  130. for line in f:
  131. match = regex.search(line.strip())
  132. if match:
  133. jemalloc_path = match.group()
  134. if not jemalloc_path:
  135. # No loaded jemalloc was found.
  136. logger.debug("jemalloc not found")
  137. return
  138. logger.debug("Found jemalloc at %s", jemalloc_path)
  139. jemalloc_dll = ctypes.CDLL(jemalloc_path)
  140. stats = JemallocStats(jemalloc_dll)
  141. _JEMALLOC_STATS = stats
  142. class JemallocCollector(Collector):
  143. """Metrics for internal jemalloc stats."""
  144. def collect(self) -> Iterable[Metric]:
  145. stats.refresh_stats()
  146. g = GaugeMetricFamily(
  147. "jemalloc_stats_app_memory_bytes",
  148. "The stats reported by jemalloc",
  149. labels=["type"],
  150. )
  151. # Read the relevant global stats from jemalloc. Note that these may
  152. # not be accurate if python is configured to use its internal small
  153. # object allocator (which is on by default, disable by setting the
  154. # env `PYTHONMALLOC=malloc`).
  155. #
  156. # See the jemalloc manpage for details about what each value means,
  157. # roughly:
  158. # - allocated ─ Total number of bytes allocated by the app
  159. # - active ─ Total number of bytes in active pages allocated by
  160. # the application, this is bigger than `allocated`.
  161. # - resident ─ Maximum number of bytes in physically resident data
  162. # pages mapped by the allocator, comprising all pages dedicated
  163. # to allocator metadata, pages backing active allocations, and
  164. # unused dirty pages. This is bigger than `active`.
  165. # - mapped ─ Total number of bytes in active extents mapped by the
  166. # allocator.
  167. # - metadata ─ Total number of bytes dedicated to jemalloc
  168. # metadata.
  169. for t in (
  170. "allocated",
  171. "active",
  172. "resident",
  173. "mapped",
  174. "metadata",
  175. ):
  176. try:
  177. value = stats.get_stat(t)
  178. except Exception as e:
  179. # There was an error fetching the value, skip.
  180. logger.warning("Failed to read jemalloc stats.%s: %s", t, e)
  181. continue
  182. g.add_metric([t], value=value)
  183. yield g
  184. REGISTRY.register(JemallocCollector())
  185. logger.debug("Added jemalloc stats")
  186. def setup_jemalloc_stats() -> None:
  187. """Try to setup jemalloc stats, if jemalloc is loaded."""
  188. try:
  189. _setup_jemalloc_stats()
  190. except Exception as e:
  191. # This should only happen if we find the loaded jemalloc library, but
  192. # fail to load it somehow (e.g. we somehow picked the wrong version).
  193. logger.info("Failed to setup collector to record jemalloc stats: %s", e)